106 Commits
v1.0.3 ... main

Author SHA1 Message Date
adbfe0aca2 chore: Release v1.2.21 2025-03-24 10:44:45 +01:00
efce228aed fix: Verbessere Datenbankinitialisierung und CSV-Import 2025-03-24 09:44:11 +01:00
d87074b9dd fix: Korrigiere Datenbankimport-Funktionen 2025-03-24 09:41:29 +01:00
efb87ce21b fix: Vermeide Neuinitialisierung der Datenbank bei jedem Start 2025-03-24 09:40:39 +01:00
4797b363cb fix: Korrigiere Datenbankpfad für Docker-Container 2025-03-24 09:35:57 +01:00
ecf4c0ee0c feat: Füge dunkles Theme und Theme-Switcher hinzu 2025-03-24 08:29:56 +01:00
10aeef2283 docs: Aktualisiere Code-Statistiken in README.md 2025-03-21 17:19:11 +01:00
d891ba62bc docs: Entwickler-Kommentare in Templates und CSS hinzugefügt 2025-03-21 17:13:57 +01:00
5549c64735 chore: Version auf 1.2.19 aktualisiert 2025-03-21 17:08:56 +01:00
392eb9de32 fix: Passwort-Eingabe in upload.html wieder hinzugefügt 2025-03-21 17:07:48 +01:00
41dfe566c6 fix: Bootstrap Icons für Menü in upload.html hinzugefügt 2025-03-21 17:04:54 +01:00
db4b0cdee2 fix: Logo-Zentrierung korrigiert 2025-03-21 17:02:04 +01:00
b0a6a7e7ca chore: Version auf 1.2.18 aktualisiert 2025-03-21 16:56:34 +01:00
9e531b3f7c feat: README-Anzeige und Code-Statistiken aktualisiert 2025-03-21 16:55:31 +01:00
99bf2fa1f4 Test 2025-03-21 13:39:12 +01:00
cd4db22e72 fix: Login-Passwort aus Umgebungsvariable LOGIN_PASSWORD verwenden 2025-03-21 13:38:04 +01:00
1cf8fbb91d Version 1.2.17: Optimierte Datenbankverbindungen und verbesserte Indizes 2025-03-21 12:56:48 +01:00
16bd7ec544 Optimierung: Thread-sichere Datenbankverbindungen und verbesserte Indizes 2025-03-21 12:55:50 +01:00
4d5a7b4f5e Version 1.2.16: Verbesserte Suchfunktion und Reset-Buttons 2025-03-21 12:42:18 +01:00
9922c0ae9d Verbesserte Suchfunktion: Highlighting für allgemeine Suche und Reset-Buttons 2025-03-21 12:40:59 +01:00
93314424d9 Feature: VCF-Export der Suchergebnisse 2025-03-21 12:07:18 +01:00
d14ac5d9af Feature: CSV-Export der Suchergebnisse 2025-03-21 12:04:17 +01:00
b4939147c4 Verbesserte index.html: JavaScript-Code in separate Datei ausgelagert und Meta-Tags hinzugefügt 2025-03-21 12:03:00 +01:00
2a33fc45de Version 1.2.16: Verbesserte Darstellung der Suchergebnisse und Code-Organisation 2025-03-21 11:50:54 +01:00
3a317de8f0 Entfernt doppelte CSS-Datei style.css 2025-03-21 11:34:28 +01:00
f626cd52fd Version 1.2.15: Autovervollständigung für Orte 2025-03-21 11:25:28 +01:00
6283ccff6d Version 1.2.14: Autovervollständigung für Fachrichtungen 2025-03-21 10:53:37 +01:00
3893e148ab README Code Statistik 2025-03-20 14:20:42 +01:00
65fbde845e Update: Code-Statistiken in README.md aktualisiert 2025-03-20 14:19:23 +01:00
57d3a05a5a Remove: docker-compose.yml aus dem Repository entfernt 2025-03-20 12:17:55 +01:00
dfd50d5e78 Entferne docker-compose.yml aus Repository 2025-03-20 12:14:33 +01:00
73541796f6 Manuelle Änderungen in .gitignore 2025-03-20 12:01:07 +01:00
e3e9900482 Version 1.2.13: Korrektur der SQL-Abfrage für die allgemeine Suche 2025-03-20 11:58:54 +01:00
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
18 changed files with 2316 additions and 1769 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

18
.gitignore vendored
View File

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

View File

@@ -5,49 +5,230 @@ 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/), 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/). und dieses Projekt adhäriert zu [Semantic Versioning](https://semver.org/lang/de/).
## [1.0.2] - 2024-03-19 ## [Unreleased]
### Hinzugefügt ### Hinzugefügt
- Wetterinformationen für jeden Suchtreffer - CSV-Export-Funktion für Suchergebnisse
- Integration der OpenWeather API - Export-Button in der Benutzeroberfläche
- Wetter-Icons und Temperaturanzeige - Automatische Formatierung der CSV-Datei mit allen relevanten Kundendaten
- Umgebungsvariablen für API-Keys
## [1.2.16] - 2024-03-21
### Geändert ### Geändert
- Anpassung der API-Antwortstruktur - Verbesserte Suchfunktion: Highlighting für allgemeine Suche in allen Feldern
- Verbesserte Fehlerbehandlung für API-Anfragen - Optimierte Reset-Buttons in den Suchfeldern
- Verbesserte CSS-Styles für die Suchfeld-Icons
## [1.0.1] - 2024-03-19
## [1.2.15] - 2024-03-20
### Hinzugefügt ### Hinzugefügt
- Neues Suchfeld für Telefonnummer - Autovervollständigung für das Ort-Feld
- Reset-Icons für alle Suchfelder - Neue API-Route für Ortsvorschläge
- Trefferzähler für Suchergebnisse - Optimierte SQL-Abfragen für die Ortssuche
## [1.2.14] - 2024-03-20
### Hinzugefügt
- Autovervollständigung für das Fachrichtungsfeld
- Neue API-Route für Fachrichtungsvorschläge
- Optimierte SQL-Abfragen für die Fachrichtungssuche
## [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 ### Geändert
- Verbesserte Positionierung der UI-Elemente - Performance-Optimierung der Suchfunktion durch Reduzierung der Suchfelder
- Optimierte Suchlogik im Backend - Verbesserte Suchgeschwindigkeit durch LIMIT in SQL-Abfragen
- CSV-Datei in data-Verzeichnis verschoben - Optimiertes Debounce-Intervall für Live-Suche
- Allgemeine Suche um Telefonnummer erweitert - 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 ### Behoben
- Korrektur der Icon-Anzeige in Suchfeldern - Problem mit fehlender führender "0" bei Telefonnummern für autorisierte Benutzer
- Verbesserte Fehlerbehandlung beim Laden der CSV-Datei
## [1.0.0] - 2024-03-18 ## [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 ### Hinzugefügt
- Grundlegende Suchfunktionalität - IP-basierte Zugriffssteuerung für medisoftware-Links
- Spezifische Suchfelder für: - IP-basierte Zugriffssteuerung für Telefonnummern-Links
- Name - Google Maps Integration für Adressen
- Ort - Version im Footer angezeigt
- Kundennummer
- Fachrichtung ### Verbessert
- Allgemeine Suche über alle Felder - Verbesserte Fehlerbehandlung bei der IP-Überprüfung
- Klickbare Links für: - Bessere Dokumentation der Konfigurationsmöglichkeiten
- Telefonnummern
- E-Mail-Adressen ## [v1.1.0] - 2024-03-17
- Google Maps Integration
- Share-Funktion für Suchergebnisse ### Hinzugefügt
- Responsive Design mit Bootstrap - Benutzer-Login für nicht-autorisierte IPs
- Live-Suche während der Eingabe - 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
## [1.2.19] - 2024-03-19
### Geändert
- Version auf 1.2.19 aktualisiert
## [1.2.18] - 2024-03-19
# ... existing code ...
## [v1.2.20] - 2024-03-19
### Hinzugefügt
- Dunkles Theme für bessere Lesbarkeit bei schlechten Lichtverhältnissen
- Theme-Switcher im Hamburger-Menü
- Automatische Speicherung der Theme-Präferenz
## [1.2.21] - 2024-03-24
### Fixed
- Verbesserte Datenbankinitialisierung und CSV-Import
- Korrektur der Suchfunktionalität
## [1.2.20] - 2024-03-24

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

184
README.md
View File

@@ -1,107 +1,159 @@
# medisoftware Kundensuche # medisoftware Kundensuche
Eine Webanwendung zur Suche in Kundendaten der medisoftware. Eine einfache und effiziente Kundensuche für medisoftware Kunden.
## Features ## Features
- Live-Suche in Kundendaten - 🔍 Echtzeit-Suche über alle Kundendaten
- Spezifische Suchfelder für: - 📱 Responsive Design für alle Geräte
- Name - 🔒 IP-basierte Zugriffskontrolle
- Ort - 🔗 Direkte Integration mit MEDISOFT
- Kundennummer - 🏥 Unterscheidung zwischen MEDISOFT und MEDICONSULT Kunden
- Fachrichtung - 🎨 Farbliche Hervorhebung der Kundentypen (blau für MEDISOFT, orange für MEDICONSULT)
- Telefonnummer - 📍 Verbesserte Adress-Links mit Location- und Route-Icons
- Allgemeine Suche über alle Felder - 📤 CSV-Export der Suchergebnisse
- Klickbare Telefonnummern - 📥 CSV-Import für Kundendaten
- Klickbare E-Mail-Adressen - 📖 Integrierte README-Anzeige
- Google Maps Integration für Adressen - 🍔 Intuitives Hamburger-Menü
- Share-Funktion für Suchergebnisse - 📱 VCF-Export für Kontakte
- Trefferzähler
- Reset-Funktion für alle Suchfelder ## Version
Aktuelle Version: 1.2.21
## Installation ## Installation
1. Repository klonen: 1. Klonen Sie das Repository:
```bash ```bash
git clone https://gitea.elpatron.me/elpatron/medi-customers.git git clone https://gitea.elpatron.me/elpatron/medi-customers.git
cd medi-customers cd medi-customers
``` ```
2. Python-Abhängigkeiten installieren: 2. Erstellen Sie die erforderlichen Verzeichnisse:
```bash ```bash
pip install -r requirements.txt mkdir -p data
``` ```
3. CSV-Datei in das `data`-Verzeichnis kopieren: 3. Starten Sie die Anwendung mit Docker Compose:
```bash ```bash
mkdir data docker-compose up --build
cp spezexpo.csv data/customers.csv
``` ```
4. Anwendung starten: Die Anwendung ist dann unter `http://localhost:5000` erreichbar.
```bash
python app.py
```
Die Anwendung ist dann unter `http://localhost:5001` erreichbar. ## Entwicklung
Die Anwendung ist in Python mit Flask entwickelt und verwendet SQLite als Datenbank. Das Frontend wurde mit HTML, CSS und JavaScript implementiert.
## 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
- `LOGIN_PASSWORD`: Das Passwort für die Login-Seite
- `UPLOAD_PASSWORD`: Das Passwort für den CSV-Upload
- `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 ## API-Beispiele
Die Such-API unterstützt folgende Parameter: ### Suche nach Name
### Spezifische Suche
```bash ```bash
# Nach Name suchen curl "http://localhost:5001/search?name=Mustermann"
curl "http://localhost:5001/search?name=Schmidt" ```
# Nach Ort suchen ### Suche nach Ort
```bash
curl "http://localhost:5001/search?ort=Berlin" curl "http://localhost:5001/search?ort=Berlin"
```
# Nach Kundennummer suchen ### Suche nach Kundennummer
```bash
curl "http://localhost:5001/search?kundennummer=12345" curl "http://localhost:5001/search?kundennummer=12345"
```
# Nach Fachrichtung suchen ### Suche nach Fachrichtung
curl "http://localhost:5001/search?fachrichtung=Allgemeinmedizin" ```bash
curl "http://localhost:5001/search?fachrichtung=Zahnarzt"
```
# Nach Telefonnummer suchen ### Suche nach Telefon
```bash
curl "http://localhost:5001/search?telefon=030" curl "http://localhost:5001/search?telefon=030"
# Kombinierte Suche
curl "http://localhost:5001/search?name=Schmidt&ort=Berlin&fachrichtung=Allgemeinmedizin"
``` ```
### Allgemeine Suche ### Allgemeine Suche
```bash ```bash
# Suche in allen Feldern curl "http://localhost:5001/search?q=Suchbegriff"
curl "http://localhost:5001/search?q=Schmidt"
``` ```
### Beispiel-Response ### Kombinierte Suche
```json ```bash
[ # Suche nach Fachrichtung und Ort
{ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin"
"Vorname": "Max",
"Nachname": "Mustermann", # Suche nach Name und Telefon
"Nummer": "12345", curl "http://localhost:5001/search?name=Mustermann&telefon=030"
"Ort": "Berlin",
"Fachrichtung": "Allgemeinmedizin", # Suche nach mehreren Kriterien
"Tel": "030123456", curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt"
"Email": "max@example.com"
}
]
``` ```
## Versionen ## Benutzeroberfläche
### v1.0.1 ### Hauptmenü
- Telefonnummer-Suchfeld hinzugefügt - Home: Zurück zur Hauptseite
- Reset-Icons für alle Suchfelder - CSV-Dateien hochladen: Import neuer Kundendaten
- Verbesserte Positionierung der UI-Elemente - README: Anzeige der Dokumentation
- Optimierte Suchlogik
- CSV-Datei in data-Verzeichnis verschoben
### v1.0.0 ### Suchfunktionen
- Erste Version - Allgemeine Suche über alle Felder
- Grundlegende Suchfunktionalität - Spezifische Suche nach:
- Klickbare Links für Telefon, E-Mail und Adressen - Name
- Share-Funktion für Suchergebnisse - Ort
- Kundennummer
- PLZ
- Fachrichtung
- Filterung nach Kundentyp (MEDISOFT/MEDICONSULT)
### Export
- CSV-Export der Suchergebnisse
- VCF-Export für Kontakte
- Direkte Links zu Kundendetails
- Teilen von Suchergebnissen
## Version
Aktuelle Version: [v1.2.17](CHANGELOG.md#v1217---2024-03-19)
## Code-Statistiken
Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------:
HTML|4|13|37|436
JavaScript|1|67|28|420
CSS|1|72|16|353
Markdown|2|79|0|300
Python|1|71|126|280
YAML|1|0|0|14
Dockerfile|1|8|9|11
Text|1|0|0|6
--------|--------|--------|--------|--------
SUM:|12|310|216|1820
## Lizenz
Alle Rechte vorbehalten. © 2025 medisoftware

498
app.py
View File

@@ -1,121 +1,451 @@
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 datetime import datetime, timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
import requests import sqlite3
from collections import defaultdict from functools import wraps
from contextlib import contextmanager
import time
import threading
import markdown2
app = Flask(__name__, static_folder='static') app = Flask(__name__, static_folder='static')
logging.basicConfig(level=logging.DEBUG) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev')
app.config['ALLOWED_IP_RANGES'] = os.getenv('ALLOWED_IP_RANGES', '192.168.0.0/16,10.0.0.0/8').split(',')
app.config['VERSION'] = '1.2.21'
app.config['DATABASE'] = 'data/customers.db'
app.config['DATABASE_TIMEOUT'] = 20
app.config['DATABASE_POOL_SIZE'] = 5
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Version der Anwendung # Thread-lokaler Speicher für Datenbankverbindungen
VERSION = "1.0.3" thread_local = threading.local()
# Pfad zur CSV-Datei def get_db_connection():
CSV_FILE = "data/customers.csv" """Erstellt eine neue Datenbankverbindung für den aktuellen Thread"""
if not hasattr(thread_local, "connection"):
thread_local.connection = sqlite3.connect(app.config['DATABASE'], timeout=app.config['DATABASE_TIMEOUT'])
thread_local.connection.row_factory = sqlite3.Row
return thread_local.connection
# Lade Umgebungsvariablen @contextmanager
load_dotenv() def get_db():
"""Context Manager für Datenbankverbindungen"""
def clean_dataframe(df): conn = get_db_connection()
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
return df.replace({np.nan: None})
# CSV-Datei laden
def load_data():
try: try:
logger.info("Versuche CSV-Datei zu laden...") yield conn
if not os.path.exists(CSV_FILE): except Exception:
logger.error(f"CSV-Datei '{CSV_FILE}' nicht gefunden!") conn.rollback()
return None raise
finally:
conn.commit()
def init_db():
"""Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
with get_db() as conn:
c = conn.cursor()
try:
# Prüfe, ob die Tabelle bereits existiert
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='customers'")
if c.fetchone() is not None:
app.logger.info('Datenbank existiert bereits, überspringe Initialisierung')
return
# 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
)
''')
# Optimierte Indizes für die häufigsten Suchanfragen
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name_ort ON customers(name, ort)')
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_plz ON customers(plz)')
# Zusammengesetzter Index für die häufigste Suchkombination
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_search ON customers(name, ort, fachrichtung, tag)')
app.logger.info('Datenbank initialisiert')
# Importiere die CSV-Daten
import_csv_data('data/medisoft.csv', 'MEDISOFT')
import_csv_data('data/mediconsult.csv', 'MEDICONSULT')
app.logger.info('CSV-Daten erfolgreich in die Datenbank importiert')
# 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)
logger.info(f"CSV-Datei erfolgreich geladen. {len(df)} Einträge gefunden.")
return df
except Exception as e: except Exception as e:
logger.error(f"Fehler beim Laden der CSV-Datei: {str(e)}") app.logger.error(f'Fehler bei der Datenbankinitialisierung: {str(e)}')
return None raise
def import_csv_data(file_path, tag):
"""Importiert die CSV-Datei in die Datenbank."""
try:
if not os.path.exists(file_path):
app.logger.warning(f"CSV-Datei {file_path} nicht gefunden")
return
app.logger.info(f"Importiere {tag}-Daten aus {file_path}...")
with get_db() as conn:
c = conn.cursor()
# Lese die CSV-Datei
df = pd.read_csv(file_path, 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)
# Filtere Datensätze mit Fachrichtung "intern"
df = df[df['Fachrichtung'].str.lower() != 'intern']
# Bereite die Daten für den Batch-Insert vor
data = [(
row['VorNachname'], row['Nummer'], row['Strasse'], row['PLZ'], row['Ort'],
row['Tel'], row['Tel'], row['mail'], row['Fachrichtung'], tag,
row['Handy'], row['Tele Firma'], row['Kontakt1'], row['Kontakt2'], row['Kontakt3']
) for _, row in df.iterrows()]
# Führe Batch-Insert durch
c.executemany('''
INSERT INTO customers (
name, nummer, strasse, plz, ort, telefon, mobil, email,
fachrichtung, tag, handy, tele_firma, kontakt1, kontakt2, kontakt3
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', data)
app.logger.info(f"{tag}-Daten erfolgreich importiert")
except Exception as e:
app.logger.error(f"Fehler beim Importieren der {tag}-Daten: {str(e)}")
raise
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
@app.route('/login', methods=['GET', 'POST'])
def login():
# Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
# Überprüfe, ob die Client-IP in einem der erlaubten Bereichen liegt
is_allowed = any(isIPInSubnet(client_ip, range.strip()) for range in app.config['ALLOWED_IP_RANGES'] if range.strip())
if is_allowed:
logger.info(f"Client-IP {client_ip} ist in einem erlaubten Bereich, automatischer Login")
session['logged_in'] = True
return redirect(url_for('index'))
if request.method == 'POST':
password = request.form.get('password')
if password == os.environ.get('LOGIN_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: {app.config['ALLOWED_IP_RANGES']}")
return render_template('index.html', allowed_ip_ranges=','.join(app.config['ALLOWED_IP_RANGES']), version=app.config['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:
# CSV-Datei laden if request.method == 'POST':
df = load_data() data = request.get_json()
if df is None: search_query = data.get('query', '')
return jsonify({"error": "Datenbank konnte nicht geladen werden"}), 500 tag = data.get('tag', 'medisoft')
# 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: else:
# Spezifische Suche search_query = request.args.get('q', '')
mask = pd.Series(True, index=df.index) 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')
with get_db() as conn:
c = conn.cursor()
# 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 = []
# Füge die Suchbedingungen hinzu
if search_query:
# Optimierte Suche mit FTS (Full Text Search)
sql_query += """
AND (
name LIKE ? OR
nummer LIKE ? OR
fachrichtung LIKE ? OR
ort LIKE ? OR
plz LIKE ? OR
strasse LIKE ? OR
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 ?
)
"""
search_term = f"%{search_query}%"
params.extend([search_term] * 16) # 16 Felder für die allgemeine Suche
if name: if name:
name_mask = ( sql_query += " AND name LIKE ?"
df['Vorname'].str.contains(name, case=False, na=False) | params.append(f"%{name}%")
df['Nachname'].str.contains(name, case=False, na=False)
)
mask &= name_mask
if ort: if ort:
ort_mask = df['Ort'].str.contains(ort, case=False, na=False) sql_query += " AND ort LIKE ?"
mask &= ort_mask params.append(f"%{ort}%")
if kundennummer: if nummer:
kundennummer_mask = df['Nummer'].astype(str).str.contains(kundennummer, case=False, na=False) sql_query += " AND nummer LIKE ?"
mask &= kundennummer_mask params.append(f"%{nummer}%")
if plz:
sql_query += " AND plz LIKE ?"
params.append(f"%{plz}%")
if fachrichtung: if fachrichtung:
fachrichtung_mask = df['Fachrichtung'].str.contains(fachrichtung, case=False, na=False) sql_query += " AND fachrichtung LIKE ?"
mask &= fachrichtung_mask params.append(f"%{fachrichtung}%")
if telefon: # Filter nach Tag
telefon_mask = df['Tel'].astype(str).str.contains(telefon, case=False, na=False) if tag != 'all':
mask &= telefon_mask sql_query += " AND tag = ?"
params.append(tag)
results = df[mask].to_dict('records') # Füge LIMIT hinzu und optimiere die Sortierung
logger.info(f"{len(results)} Ergebnisse gefunden") sql_query += " ORDER BY name LIMIT 100"
# Führe die Abfrage aus
c.execute(sql_query, params)
results = c.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)
return jsonify(formatted_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
@app.route('/api/fachrichtungen')
def get_fachrichtungen():
try:
search_term = request.args.get('q', '').lower()
with get_db() as conn:
c = conn.cursor()
# Hole alle eindeutigen Fachrichtungen, die mit dem Suchbegriff übereinstimmen
c.execute('''
SELECT DISTINCT fachrichtung
FROM customers
WHERE fachrichtung IS NOT NULL
AND fachrichtung != ''
AND LOWER(fachrichtung) LIKE ?
ORDER BY fachrichtung
LIMIT 10
''', (f'%{search_term}%',))
fachrichtungen = [row[0] for row in c.fetchall()]
return jsonify(fachrichtungen)
except Exception as e:
logger.error(f"Fehler beim Abrufen der Fachrichtungen: {str(e)}")
return jsonify([])
@app.route('/api/orte')
def get_orte():
try:
search_term = request.args.get('q', '').lower()
with get_db() as conn:
c = conn.cursor()
# Hole alle eindeutigen Orte, die mit dem Suchbegriff übereinstimmen
c.execute('''
SELECT DISTINCT ort
FROM customers
WHERE ort IS NOT NULL
AND ort != ''
AND LOWER(ort) LIKE ?
ORDER BY ort
LIMIT 10
''', (f'%{search_term}%',))
orte = [row[0] for row in c.fetchall()]
return jsonify(orte)
except Exception as e:
logger.error(f"Fehler beim Abrufen der Orte: {str(e)}")
return jsonify([])
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if not session.get('logged_in'):
return redirect(url_for('login'))
if request.method == 'POST':
if request.form.get('password') != os.environ.get('UPLOAD_PASSWORD'):
return render_template('upload.html', error="Falsches Passwort", version=app.config['VERSION'])
if 'customers_snk' not in request.files or 'customers' not in request.files:
return render_template('upload.html', error="Bitte beide CSV-Dateien auswählen", version=app.config['VERSION'])
customers_snk = request.files['customers_snk']
customers = request.files['customers']
if customers_snk.filename == '' or customers.filename == '':
return render_template('upload.html', error="Keine Datei ausgewählt", version=app.config['VERSION'])
if not customers_snk.filename.endswith('.csv') or not customers.filename.endswith('.csv'):
return render_template('upload.html', error="Nur CSV-Dateien sind erlaubt", version=app.config['VERSION'])
try:
# Speichere die Dateien
customers_snk.save('data/customers_snk.csv')
customers.save('data/customers.csv')
# Importiere die Daten in die Datenbank
import_csv_data('data/customers_snk.csv', 'snk')
import_csv_data('data/customers.csv', 'medisoft')
return render_template('upload.html', success="Dateien erfolgreich hochgeladen und importiert", version=app.config['VERSION'])
except Exception as e:
logger.error(f"Fehler beim Upload: {str(e)}")
return render_template('upload.html', error=f"Fehler beim Upload: {str(e)}", version=app.config['VERSION'])
return render_template('upload.html', version=app.config['VERSION'])
@app.route('/readme')
def readme():
if not session.get('logged_in'):
return redirect(url_for('login'))
try:
with open('README.md', 'r', encoding='utf-8') as f:
content = f.read()
html_content = markdown2.markdown(content, extras=['fenced-code-blocks', 'tables'])
return render_template('readme.html', content=html_content, version=app.config['VERSION'])
except Exception as e:
logger.error(f"Fehler beim Lesen der README: {str(e)}")
return render_template('readme.html', error="Fehler beim Lesen der README", version=app.config['VERSION'])
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)
# Initialisiere die Datenbank
init_db()
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, port=5001) app.run(debug=True, port=5001)

File diff suppressed because it is too large Load Diff

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,14 @@
services:
web:
build: .
ports:
- "5001:5000"
volumes:
- ./data:/app/data
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
- LOGIN_PASSWORD=changeme
- UPLOAD_PASSWORD=upload_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

@@ -3,3 +3,4 @@ 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 requests==2.32.3
markdown2==2.4.12

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

@@ -0,0 +1,87 @@
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--background-color: #ffffff;
--text-color: #212529;
--card-bg: #ffffff;
--border-color: #dee2e6;
--hover-bg: #f8f9fa;
--shadow-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--primary-color: #0d6efd;
--secondary-color: #adb5bd;
--background-color: #212529;
--text-color: #f8f9fa;
--card-bg: #343a40;
--border-color: #495057;
--hover-bg: #495057;
--shadow-color: rgba(0, 0, 0, 0.3);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
box-shadow: 0 2px 4px var(--shadow-color);
}
.table {
color: var(--text-color);
}
.table thead th {
background-color: var(--card-bg);
border-bottom-color: var(--border-color);
color: var(--text-color);
}
.table td {
border-color: var(--border-color);
}
.table-hover tbody tr:hover {
background-color: var(--hover-bg);
}
.modal-content {
background-color: var(--card-bg);
color: var(--text-color);
border-color: var(--border-color);
}
.modal-header {
border-bottom-color: var(--border-color);
}
.modal-footer {
border-top-color: var(--border-color);
}
.form-control {
background-color: var(--card-bg);
border-color: var(--border-color);
color: var(--text-color);
}
.form-control:focus {
background-color: var(--card-bg);
color: var(--text-color);
}
.btn-outline-secondary {
color: var(--text-color);
border-color: var(--border-color);
}
.btn-outline-secondary:hover {
background-color: var(--hover-bg);
color: var(--text-color);
}

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

@@ -0,0 +1,441 @@
/*
medisoftware Kundensuche - Hauptstyles
Version: 1.2.19
Entwickler: medisoftware GmbH
Letzte Änderung: 2024-03-19
*/
/* Allgemeine Styles */
body {
min-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
background-color: #f8f9fa;
}
/* Hauptcontainer */
.main-content {
flex: 1 0 auto;
padding: 2rem 0;
margin-bottom: 4rem;
}
/* Suchcontainer */
.search-container {
background-color: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
/* Suchergebnisse */
#searchResults {
margin-top: 2rem;
}
/* Kundenkarte */
.customer-card {
background-color: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.customer-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
/* Kundenname */
.customer-name {
font-size: 1.25rem;
font-weight: 600;
color: #333;
margin-bottom: 0.5rem;
}
/* Kundeninformationen */
.customer-info {
color: #666;
margin-bottom: 0.5rem;
}
/* Klickbare Links */
.customer-info a {
color: #0d6efd;
text-decoration: none;
}
.customer-info a:hover {
text-decoration: underline;
}
/* Footer */
.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;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.footer-link {
color: #0d6efd;
text-decoration: none;
}
.footer-link:hover {
text-decoration: underline;
}
/* Responsive Anpassungen */
@media (max-width: 768px) {
.main-content {
padding: 1rem 0;
}
.search-container {
padding: 1rem;
}
.customer-card {
padding: 1rem;
}
}
.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;
}
.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;
}
.reset-icon {
position: absolute;
right: 40px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #6c757d;
z-index: 10;
padding: 0.375rem;
display: none;
}
.input-group input:not(:placeholder-shown) + .reset-icon {
display: block;
}
.reset-icon:hover {
color: #dc3545;
}
.search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #6c757d;
z-index: 10;
padding: 0.375rem;
}
.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-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.customer-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.customer-details {
font-size: 0.9rem;
color: #666;
}
.customer-details p {
margin: 0.25rem 0;
}
.customer-details strong {
color: #333;
}
.phone-link, .email-link, .customer-link {
color: #007bff;
text-decoration: none;
}
.phone-link:hover, .email-link:hover, .customer-link:hover {
text-decoration: underline;
}
.address-text {
margin-right: 0.5rem;
}
.address-link, .route-link {
color: #6c757d;
text-decoration: none;
margin-left: 0.5rem;
}
.address-link:hover, .route-link:hover {
color: #343a40;
}
.location-pin, .route-pin {
font-size: 0.9rem;
}
.badge {
font-size: 0.8rem;
padding: 0.35em 0.65em;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.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;
}
.autocomplete-items {
position: absolute;
border: 1px solid #d4d4d4;
border-top: none;
z-index: 99;
top: 100%;
left: 0;
right: 0;
background-color: white;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.autocomplete-items div {
padding: 8px 12px;
cursor: pointer;
background-color: white;
}
.autocomplete-items div:hover {
background-color: #f8f9fa;
}

515
static/js/main.js Normal file
View File

@@ -0,0 +1,515 @@
let searchTimeout;
let lastResults = [];
let fachrichtungTimeout;
let ortTimeout;
let currentPage = 1;
let totalPages = 1;
let currentResults = [];
let currentSearchQuery = '';
let currentFilters = {
fachrichtung: '',
plz: '',
ort: ''
};
function createPhoneLink(phone) {
if (!phone) return '';
const clientIP = document.querySelector('meta[name="client-ip"]').content;
const allowedIPRanges = document.querySelector('meta[name="allowed-ip-ranges"]').content.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) {
if (!email) return '';
return `<a href="mailto:${email}" class="email-link">${email}</a>`;
}
function highlightText(text, searchTerm) {
if (!searchTerm || !text) return text;
// Teile den Suchbegriff in einzelne Wörter
const searchWords = searchTerm.split(/\s+/).filter(word => word.length > 0);
// Wenn keine Wörter gefunden wurden, gebe den ursprünglichen Text zurück
if (searchWords.length === 0) return text;
// Erstelle einen regulären Ausdruck für alle Suchwörter
const regexPattern = searchWords
.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('|');
// Erstelle den regulären Ausdruck
const regex = new RegExp(`(${regexPattern})`, 'gi');
// Ersetze alle Übereinstimmungen mit mark-Tags
return text.replace(regex, '<mark>$1</mark>');
}
function createAddressLink(street, plz, city) {
if (!street || !plz || !city) return '';
const address = `${street}, ${plz} ${city}`;
const searchQuery = encodeURIComponent(address);
const routeQuery = encodeURIComponent(address);
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">
<i class="fa-solid fa-location-dot location-pin"></i>
</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>`;
}
function adjustCustomerNumber(number) {
return number - 12000;
}
function isIPInSubnet(ip, subnet) {
// 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 = document.querySelector('meta[name="client-ip"]').content;
const allowedIPRanges = document.querySelector('meta[name="allowed-ip-ranges"]').content.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() {
const feedback = document.getElementById('shareFeedback');
feedback.style.display = 'block';
feedback.style.opacity = '1';
feedback.addEventListener('animationend', () => {
feedback.style.display = 'none';
}, { once: true });
}
async function copyCustomerLink(customerNumber) {
const url = new URL(window.location.href);
url.searchParams.set('kundennummer', customerNumber);
try {
await navigator.clipboard.writeText(url.toString());
showCopyFeedback();
} catch (err) {
// Fehlerbehandlung ohne console.log
}
}
function updateResultCounts() {
const resultCount = document.getElementById('result-count');
const exportButton = document.getElementById('exportButton');
if (lastResults && lastResults.length > 0) {
resultCount.textContent = `${lastResults.length} Ergebnisse gefunden`;
resultCount.style.display = 'inline';
exportButton.style.display = 'inline-block';
} else {
resultCount.textContent = '';
resultCount.style.display = 'none';
exportButton.style.display = 'none';
}
}
function exportToCSV() {
if (!lastResults || lastResults.length === 0) return;
// CSV-Header definieren
const headers = [
'Nummer',
'Name',
'Fachrichtung',
'Straße',
'PLZ',
'Ort',
'Telefon',
'Mobil',
'Handy',
'Telefon Firma',
'E-Mail',
'Kontakt 1',
'Kontakt 2',
'Kontakt 3',
'Tags'
];
// CSV-Daten erstellen
const csvRows = [headers];
lastResults.forEach(customer => {
const row = [
customer.nummer,
customer.name,
customer.fachrichtung,
customer.strasse,
customer.plz,
customer.ort,
customer.telefon,
customer.mobil,
customer.handy,
customer.tele_firma,
customer.email,
customer.kontakt1,
customer.kontakt2,
customer.kontakt3,
(customer.tags || []).join(';')
].map(value => {
// Werte mit Kommas oder Anführungszeichen in Anführungszeichen setzen
if (value && (value.includes(',') || value.includes('"') || value.includes('\n'))) {
return `"${value.replace(/"/g, '""')}"`;
}
return value || '';
});
csvRows.push(row);
});
// CSV-String erstellen
const csvContent = csvRows.map(row => row.join(',')).join('\n');
// Blob erstellen und Download starten
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `kundensuche_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function exportToVCF(customer) {
if (!customer) return;
const vcfData = [
'BEGIN:VCARD',
'VERSION:3.0',
`FN:${customer.name || ''}`,
`N:${customer.name || ''};;;`,
`TEL;TYPE=CELL:${customer.telefon || ''}`,
`TEL;TYPE=HOME:${customer.mobil || ''}`,
`EMAIL:${customer.email || ''}`,
`ADR;TYPE=HOME:;;${customer.strasse || ''};${customer.plz || ''};${customer.ort || ''};`,
`ORG:${customer.fachrichtung || ''}`,
'END:VCARD'
].join('\n');
const blob = new Blob([vcfData], { type: 'text/vcard;charset=utf-8' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `kontakt_${customer.name || ''}_${new Date().toISOString().split('T')[0]}.vcf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
function displayResults(results) {
const resultsDiv = document.getElementById('results');
const resultCount = document.getElementById('result-count');
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 = '';
return;
}
resultCount.textContent = `${results.length} Ergebnisse`;
lastResults = results;
const resultsHTML = 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>`;
};
// Highlighting für alle Felder
const highlightField = (value) => {
if (!value) return value;
let highlighted = value;
if (nameSearchTerm) {
highlighted = highlightText(highlighted, nameSearchTerm);
}
if (fachrichtungSearchTerm) {
highlighted = highlightText(highlighted, fachrichtungSearchTerm);
}
if (generalSearchTerm) {
highlighted = highlightText(highlighted, generalSearchTerm);
}
return highlighted;
};
return `
<div class="customer-card">
<div class="customer-header">
<h3 class="customer-name">${highlightField(customer.name)}</h3>
<div class="customer-actions">
<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}')" title="Link kopieren">
<i class="fas fa-link"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick='exportToVCF(${JSON.stringify(customer).replace(/'/g, "\\'")})' title="Als VCF exportieren">
<i class="bi bi-person-vcard"></i>
</button>
</div>
</div>
<div class="customer-details">
${createFieldIfValue('Nummer', highlightField(customer.nummer), createCustomerLink)}
${createFieldIfValue('Adresse', (customer.strasse && customer.plz && customer.ort) ? true : false,
() => createAddressLink(
customer.strasse,
highlightField(customer.plz),
highlightField(customer.ort)
))}
${createFieldIfValue('Telefon', highlightField(customer.telefon), createPhoneLink)}
${createFieldIfValue('Mobil', highlightField(customer.mobil), createPhoneLink)}
${createFieldIfValue('Handy', highlightField(customer.handy), createPhoneLink)}
${createFieldIfValue('E-Mail', highlightField(customer.email), createEmailLink)}
${createFieldIfValue('Fachrichtung', highlightField(customer.fachrichtung))}
${createFieldIfValue('Kontakt 1', highlightField(customer.kontakt1), createPhoneLink)}
${createFieldIfValue('Kontakt 2', highlightField(customer.kontakt2), createPhoneLink)}
${createFieldIfValue('Kontakt 3', highlightField(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>
`;
}).join('');
resultsDiv.innerHTML = resultsHTML;
updateResultCounts();
}
function clearInput(inputId) {
document.getElementById(inputId).value = '';
document.getElementById('results').innerHTML = '';
document.getElementById('result-count').textContent = '';
document.getElementById('exportButton').style.display = 'none';
lastResults = [];
}
async function searchCustomers() {
let searchTimeout;
const loading = document.getElementById('loading');
const results = document.getElementById('results');
const generalSearch = document.getElementById('q').value;
const nameSearch = document.getElementById('nameInput').value;
const ortSearch = document.getElementById('ortInput').value;
const nummerSearch = document.getElementById('nummerInput').value;
const plzSearch = document.getElementById('plzInput').value;
const fachrichtungSearch = document.getElementById('fachrichtungInput').value;
const tagFilter = document.getElementById('tagFilter').value;
currentSearchQuery = generalSearch;
currentPage = 1;
// Zeige Ladeanimation
loading.style.display = 'block';
results.innerHTML = '';
// Setze Timeout zurück
clearTimeout(searchTimeout);
// Verzögerte Suche
searchTimeout = setTimeout(async () => {
try {
// Baue die Suchanfrage
const params = new URLSearchParams();
if (generalSearch) params.append('q', generalSearch);
if (nameSearch) params.append('name', nameSearch);
if (ortSearch) params.append('ort', ortSearch);
if (nummerSearch) params.append('nummer', nummerSearch);
if (plzSearch) params.append('plz', plzSearch);
if (fachrichtungSearch) params.append('fachrichtung', fachrichtungSearch);
if (tagFilter) params.append('tag', tagFilter);
const response = await fetch('/search?' + params.toString());
if (!response.ok) {
throw new Error('Netzwerkantwort war nicht ok');
}
const data = await response.json();
displayResults(data);
} catch (error) {
results.innerHTML = '<p>Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</p>';
} finally {
loading.style.display = 'none';
}
}, 300);
}
function setupFachrichtungAutocomplete() {
const fachrichtungInput = document.getElementById('fachrichtungInput');
const autocompleteList = document.createElement('div');
autocompleteList.className = 'autocomplete-items';
fachrichtungInput.parentNode.appendChild(autocompleteList);
fachrichtungInput.addEventListener('input', function() {
clearTimeout(fachrichtungTimeout);
const searchTerm = this.value;
if (searchTerm.length < 2) {
autocompleteList.style.display = 'none';
return;
}
fachrichtungTimeout = setTimeout(() => {
fetch(`/api/fachrichtungen?q=${encodeURIComponent(searchTerm)}`)
.then(response => response.json())
.then(data => {
autocompleteList.innerHTML = '';
if (data.length > 0) {
data.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
div.addEventListener('click', () => {
fachrichtungInput.value = item;
autocompleteList.style.display = 'none';
searchCustomers();
});
autocompleteList.appendChild(div);
});
autocompleteList.style.display = 'block';
} else {
autocompleteList.style.display = 'none';
}
});
}, 300);
});
document.addEventListener('click', function(e) {
if (!fachrichtungInput.contains(e.target) && !autocompleteList.contains(e.target)) {
autocompleteList.style.display = 'none';
}
});
}
function setupOrtAutocomplete() {
const ortInput = document.getElementById('ortInput');
const autocompleteList = document.createElement('div');
autocompleteList.className = 'autocomplete-items';
ortInput.parentNode.appendChild(autocompleteList);
ortInput.addEventListener('input', function() {
clearTimeout(ortTimeout);
const searchTerm = this.value;
if (searchTerm.length < 2) {
autocompleteList.style.display = 'none';
return;
}
ortTimeout = setTimeout(() => {
fetch(`/api/orte?q=${encodeURIComponent(searchTerm)}`)
.then(response => response.json())
.then(data => {
autocompleteList.innerHTML = '';
if (data.length > 0) {
data.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
div.addEventListener('click', () => {
ortInput.value = item;
autocompleteList.style.display = 'none';
searchCustomers();
});
autocompleteList.appendChild(div);
});
autocompleteList.style.display = 'block';
} else {
autocompleteList.style.display = 'none';
}
});
}, 300);
});
document.addEventListener('click', function(e) {
if (!ortInput.contains(e.target) && !autocompleteList.contains(e.target)) {
autocompleteList.style.display = 'none';
}
});
}
// Event-Listener für die URL-Parameter und Autocomplete-Setup
document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
const kundennummer = urlParams.get('kundennummer');
const name = urlParams.get('name');
const ort = urlParams.get('ort');
const plz = urlParams.get('plz');
if (kundennummer) {
document.getElementById('nummerInput').value = kundennummer;
searchCustomers();
}
if (name) {
document.getElementById('nameInput').value = name;
searchCustomers();
}
if (ort) {
document.getElementById('ortInput').value = ort;
searchCustomers();
}
if (plz) {
document.getElementById('plzInput').value = plz;
searchCustomers();
}
// Setup Autocomplete
setupFachrichtungAutocomplete();
setupOrtAutocomplete();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -3,198 +3,132 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="client-ip" content="{{ request.headers.get('X-Forwarded-For', request.remote_addr) }}">
<meta name="allowed-ip-ranges" content="{{ allowed_ip_ranges }}">
<!--
medisoftware Kundensuche
Version: {{ version }}
Entwickler: medisoftware GmbH
Letzte Änderung: 2024-03-19
-->
<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"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
<style> <style>
body { /*
min-height: 100vh; Inline-Styles für spezifische Anpassungen
display: flex; Diese Styles sind nur für diese Seite gültig
flex-direction: column; */
margin: 0; .logo {
padding: 0; width: 200px;
} height: auto;
.main-content {
flex: 1 0 auto;
padding: 2rem 0;
}
.search-container {
max-width: 800px;
margin: 0 auto; 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; 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%;
}
.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;
}
.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>
<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="position-relative mb-4">
<div class="dropdown position-absolute start-0">
<button class="btn btn-link text-dark" type="button" id="menuButton" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-list fs-4"></i>
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ url_for('index') }}">
<i class="bi bi-house"></i> Home
</a>
<a class="dropdown-item" href="{{ url_for('upload') }}">
<i class="bi bi-cloud-upload"></i> CSV-Dateien hochladen
</a>
<a class="dropdown-item" href="{{ url_for('readme') }}">
<i class="bi bi-book"></i> README
</a>
<li>
<button class="dropdown-item theme-toggle" id="themeToggle">
<i class="bi bi-moon-stars"></i> Theme wechseln
</button>
</li>
</div>
</div>
<div class="text-center">
<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 logo">
</a>
</div>
</div>
<div class="search-container">
<h1 class="text-center mb-4">Kundensuche</h1>
<div class="input-group mb-4 position-relative"> <div class="general-search mb-4">
<input type="text" id="searchInput" class="form-control form-control-lg" <div class="input-group">
placeholder="Allgemeine Suche..."> <input type="text" id="q" class="form-control form-control-lg" placeholder="Allgemeine Suche" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="searchReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('q')"></i>
<span class="search-icon">🔍</span> <i class="fas fa-search search-icon"></i>
</div>
</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()">
<i class="fa-solid fa-xmark reset-icon" id="nameReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('nameInput')"></i>
</div> <i class="fas fa-search search-icon"></i>
<div class="search-field">
<input type="text" id="ortInput" class="form-control"
placeholder="Ort...">
<i class="fa-solid fa-xmark reset-icon" id="ortReset"></i>
</div>
<div class="search-field">
<input type="text" id="kundennummerInput" class="form-control"
placeholder="Kundennummer...">
<i class="fa-solid fa-xmark reset-icon" id="kundennummerReset"></i>
</div>
<div class="search-field">
<input type="text" id="fachrichtungInput" class="form-control"
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="result-counts"> <div class="search-field">
<span id="generalCount" class="result-count"></span> <div class="input-group">
<input type="text" id="ortInput" class="form-control" placeholder="Ort" oninput="searchCustomers()">
<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 id="result-counts" class="mt-2">
<span id="result-count"></span>
<button id="exportButton" class="btn btn-sm btn-outline-primary ms-2" onclick="exportToCSV()" style="display: none;">
<i class="bi bi-file-earmark-spreadsheet"></i> Als CSV exportieren
</button>
</div> </div>
<div id="loading" class="loading"> <div id="loading" class="loading">
@@ -203,230 +137,46 @@
</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 src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
let searchTimeout; // Theme Switcher
let lastResults = []; const themeToggle = document.getElementById('themeToggle');
const icon = themeToggle.querySelector('i');
function createPhoneLink(phone) { // Theme aus dem localStorage laden
if (!phone) return 'N/A'; const savedTheme = localStorage.getItem('theme') || 'light';
const cleaned = phone.replace(/\D/g, ''); document.documentElement.setAttribute('data-theme', savedTheme);
const telLink = '0' + cleaned; updateThemeIcon(savedTheme);
return `<a href="tel:${telLink}" class="phone-link">${phone}</a>`;
}
function createEmailLink(email) { themeToggle.addEventListener('click', () => {
if (!email) return 'N/A'; const currentTheme = document.documentElement.getAttribute('data-theme');
return `<a href="mailto:${email}" class="email-link">${email}</a>`; const newTheme = currentTheme === 'light' ? 'dark' : 'light';
}
function createAddressLink(street, plz, city) { document.documentElement.setAttribute('data-theme', newTheme);
if (!street || !plz || !city) return 'N/A'; localStorage.setItem('theme', newTheme);
const address = `${street}, ${plz} ${city}`; updateThemeIcon(newTheme);
const searchQuery = encodeURIComponent(address);
return `${address}
<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}"
class="address-link" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-location-pin location-pin"></i>
</a>`;
}
function createCustomerLink(customerNumber) {
if (!customerNumber) return 'N/A';
return `<a href="medisw:openkkbefe/P${customerNumber}?NetGrp=4"
class="customer-link" target="_blank" rel="noopener noreferrer">
${customerNumber}
</a>`;
}
function showCopyFeedback() {
const feedback = document.getElementById('shareFeedback');
feedback.style.display = 'block';
feedback.style.opacity = '1';
feedback.addEventListener('animationend', () => {
feedback.style.display = 'none';
}, { once: true });
}
async function copyCustomerLink(customerNumber) {
const url = new URL(window.location.href);
url.searchParams.set('kundennummer', customerNumber);
try {
await navigator.clipboard.writeText(url.toString());
showCopyFeedback();
} catch (err) {
console.error('Fehler beim Kopieren:', err);
}
}
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() {
const name = document.getElementById('nameInput').value;
const ort = document.getElementById('ortInput').value;
const kundennummer = document.getElementById('kundennummerInput').value;
const fachrichtung = document.getElementById('fachrichtungInput').value;
const telefon = document.getElementById('telefonInput').value;
const query = document.getElementById('searchInput').value;
// Prüfe, ob mindestens ein Suchfeld ausgefüllt ist
if (!name && !ort && !kundennummer && !fachrichtung && !telefon && !query) {
document.getElementById('results').innerHTML = '';
lastResults = [];
updateResultCounts();
return;
}
// Lade-Animation anzeigen
document.getElementById('loading').style.display = 'block';
document.getElementById('results').innerHTML = '';
// URL-Parameter erstellen
const params = new URLSearchParams();
if (name) params.append('name', name);
if (ort) params.append('ort', ort);
if (kundennummer) params.append('kundennummer', kundennummer);
if (fachrichtung) params.append('fachrichtung', fachrichtung);
if (telefon) params.append('telefon', telefon);
if (query) params.append('q', query);
fetch(`/search?${params.toString()}`)
.then(response => response.json())
.then(data => {
if (data.error) {
showError(data.error);
return;
}
const resultsContainer = document.getElementById('results');
resultsContainer.innerHTML = '';
if (data.results && data.results.length > 0) {
data.results.forEach(customer => {
const card = document.createElement('div');
card.className = 'card mb-3';
card.innerHTML = `
<div class="card-body">
<h5 class="card-title">${customer.Vorname} ${customer.Nachname}</h5>
<p class="card-text">
<strong>Kundennummer:</strong> ${customer.Nummer}<br>
<strong>Fachrichtung:</strong> ${customer.Fachrichtung || 'N/A'}<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>E-Mail:</strong> ${createEmailLink(customer.mail)}
</p>
</div>
`;
resultsContainer.appendChild(card);
}); });
// Zeige die Anzahl der Treffer an function updateThemeIcon(theme) {
const totalResults = document.getElementById('total-results'); icon.className = theme === 'light' ? 'bi bi-moon-stars' : 'bi bi-sun';
if (totalResults) {
totalResults.textContent = `${data.total} Treffer gefunden`;
} }
} else {
resultsContainer.innerHTML = '<div class="alert alert-info">Keine Ergebnisse gefunden.</div>';
}
})
.catch(error => {
console.error('Fehler:', error);
document.getElementById('results').innerHTML =
`<div class="alert alert-danger">${error.message}</div>`;
lastResults = [];
updateResultCounts();
})
.finally(() => {
document.getElementById('loading').style.display = 'none';
});
}
// Event-Listener für die Live-Suche
const searchInputs = [
document.getElementById('nameInput'),
document.getElementById('ortInput'),
document.getElementById('kundennummerInput'),
document.getElementById('fachrichtungInput'),
document.getElementById('telefonInput'),
document.getElementById('searchInput')
];
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() {
clearTimeout(searchTimeout);
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();
});
});
// URL-Parameter beim Laden der Seite prüfen
window.addEventListener('load', function() {
const urlParams = new URLSearchParams(window.location.search);
const name = urlParams.get('name');
const ort = urlParams.get('ort');
const kundennummer = urlParams.get('kundennummer');
const fachrichtung = urlParams.get('fachrichtung');
const telefon = urlParams.get('telefon');
const query = urlParams.get('q');
if (name) document.getElementById('nameInput').value = name;
if (ort) document.getElementById('ortInput').value = ort;
if (kundennummer) document.getElementById('kundennummerInput').value = kundennummer;
if (fachrichtung) document.getElementById('fachrichtungInput').value = fachrichtung;
if (telefon) document.getElementById('telefonInput').value = telefon;
if (query) document.getElementById('searchInput').value = query;
if (name || ort || kundennummer || fachrichtung || telefon || query) {
searchCustomers();
}
});
</script> </script>
</body> </body>
</html> </html>

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>

143
templates/readme.html Normal file
View File

@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--
medisoftware Kundensuche - README
Version: {{ version }}
Entwickler: medisoftware GmbH
Letzte Änderung: 2024-03-19
-->
<title>medisoftware Kundensuche - README</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons für die Menü-Symbole -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
/*
Inline-Styles für die README-Seite
Diese Styles sind nur für diese Seite gültig
*/
body {
background-color: #f8f9fa;
}
.main-content {
padding: 2rem 0;
}
.logo {
width: 200px;
height: auto;
margin: 0 auto;
display: block;
}
.readme-container {
background-color: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.readme-content {
max-width: 800px;
margin: 0 auto;
}
.readme-content h1 {
color: #333;
margin-bottom: 1.5rem;
}
.readme-content h2 {
color: #444;
margin-top: 2rem;
margin-bottom: 1rem;
}
.readme-content p {
line-height: 1.6;
margin-bottom: 1rem;
}
.readme-content code {
background-color: #f8f9fa;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: monospace;
}
.readme-content pre {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 5px;
overflow-x: auto;
}
.readme-content ul, .readme-content ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
.readme-content li {
margin-bottom: 0.5rem;
}
.readme-content blockquote {
border-left: 4px solid #dee2e6;
padding-left: 1rem;
margin-left: 0;
color: #6c757d;
}
.readme-content table {
width: 100%;
margin-bottom: 1rem;
border-collapse: collapse;
}
.readme-content th, .readme-content td {
padding: 0.5rem;
border: 1px solid #dee2e6;
}
.readme-content th {
background-color: #f8f9fa;
}
</style>
</head>
<body>
<!-- Hauptcontainer für den Inhalt -->
<div class="main-content">
<div class="container">
<!-- Header mit Logo und Menü -->
<div class="position-relative mb-4">
<!-- Dropdown-Menü -->
<div class="dropdown position-absolute start-0">
<button class="btn btn-link text-dark" type="button" id="menuButton" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-list fs-4"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="menuButton">
<li>
<a class="dropdown-item" href="{{ url_for('index') }}">
<i class="bi bi-house"></i> Home
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('upload') }}">
<i class="bi bi-cloud-upload"></i> CSV-Dateien hochladen
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('readme') }}">
<i class="bi bi-book"></i> README
</a>
</li>
</ul>
</div>
<!-- Logo -->
<div class="text-center">
<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 logo">
</a>
</div>
</div>
<!-- README-Container -->
<div class="readme-container">
<div class="readme-content">
{{ readme_content | safe }}
</div>
</div>
</div>
</div>
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

123
templates/upload.html Normal file
View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--
medisoftware Kundensuche - CSV Upload
Version: {{ version }}
Entwickler: medisoftware GmbH
Letzte Änderung: 2024-03-19
-->
<title>medisoftware Kundensuche - CSV Upload</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons für die Menü-Symbole -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
/*
Inline-Styles für die Upload-Seite
Diese Styles sind nur für diese Seite gültig
*/
body {
background-color: #f8f9fa;
}
.main-content {
padding: 2rem 0;
}
.logo {
width: 200px;
height: auto;
margin: 0 auto;
display: block;
}
.upload-container {
background-color: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.form-label {
font-weight: 500;
}
.alert {
margin-bottom: 1rem;
}
</style>
</head>
<body>
<!-- Hauptcontainer für den Inhalt -->
<div class="main-content">
<div class="container">
<!-- Header mit Logo und Menü -->
<div class="position-relative mb-4">
<!-- Dropdown-Menü -->
<div class="dropdown position-absolute start-0">
<button class="btn btn-link text-dark" type="button" id="menuButton" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-list fs-4"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="menuButton">
<li>
<a class="dropdown-item" href="{{ url_for('index') }}">
<i class="bi bi-house"></i> Home
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('upload') }}">
<i class="bi bi-cloud-upload"></i> CSV-Dateien hochladen
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('readme') }}">
<i class="bi bi-book"></i> README
</a>
</li>
</ul>
</div>
<!-- Logo -->
<div class="text-center">
<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 logo">
</a>
</div>
</div>
<!-- Upload-Container -->
<div class="upload-container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 class="text-center mb-4">CSV-Dateien hochladen</h2>
<!-- Fehlermeldungen -->
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<!-- Erfolgsmeldungen -->
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
<!-- Upload-Formular -->
<form method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="medisoft_file" class="form-label">MEDISOFT CSV-Datei</label>
<input type="file" class="form-control" id="medisoft_file" name="medisoft_file" accept=".csv">
</div>
<div class="mb-3">
<label for="mediconsult_file" class="form-label">MEDICONSULT CSV-Datei</label>
<input type="file" class="form-control" id="mediconsult_file" name="mediconsult_file" accept=".csv">
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Dateien hochladen</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>