15 Commits

7 changed files with 209 additions and 80 deletions

View File

@@ -5,6 +5,21 @@ 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/).
## [v1.2.0] - 2024-03-17
### Hinzugefügt
- Benutzer-Login-Funktionalität
- Login-Seite mit Passwortüberprüfung
- Umgebungsvariable für Login-Passwort
## [v1.1.0] - 2024-03-17
### Geändert
- Verbesserte Darstellung der Telefonnummern:
- Separate Felder für private Telefonnummer, Firmennummer und Mobilfunknummer
- Entfernung der Faxnummer aus der Anzeige
- Übersichtlichere Darstellung der Kontaktinformationen
## [1.0.6] - 2024-03-17 ## [1.0.6] - 2024-03-17
### Geändert ### Geändert
- Verbesserte Suchfunktion: Kombinierte Suche über mehrere Felder möglich - Verbesserte Suchfunktion: Kombinierte Suche über mehrere Felder möglich
@@ -34,8 +49,10 @@ und dieses Projekt adhäriert zu [Semantic Versioning](https://semver.org/lang/d
- Wetterinformationen für Kundensitz - Wetterinformationen für Kundensitz
- Caching für Wetterdaten - Caching für Wetterdaten
## [1.0.0] - 2024-03-17 ## [v1.0.0] - 2024-03-17
### Hinzugefügt ### Hinzugefügt
- Erste Version der Kundensuche - Erste Version der Kundensuche
- Grundlegende Suchfunktionen - Grundlegende Suchfunktionen
- Responsive Design - Responsive Design
- Docker-Integration

View File

@@ -1,27 +1,14 @@
# medisoftware Kundensuche # medisoftware Kundensuche
Eine webbasierte Kundensuche für medisoftware mit erweiterten Suchfunktionen. Eine webbasierte Suchanwendung für medisoftware Kunden, die eine schnelle und effiziente Suche nach Kundendaten ermöglicht.
## Features ## Features
- Schnelle und präzise Kundensuche - Echtzeit-Suche über Kundendaten
- Mehrere Suchfelder für gezielte Suche: - Hervorhebung von Suchbegriffen in den Ergebnissen
- Name (Vor- und Nachname) - Klickbare Links für Telefonnummern, E-Mail-Adressen und Adressen
- Ort - Responsive Design für mobile Geräte
- Kundennummer - Docker-Container für einfache Installation und Deployment
- Fachrichtung
- Telefon
- Allgemeine Suche über alle Felder
- Kombinierte Suche über mehrere Felder
- Hervorhebung der Suchbegriffe in den Ergebnissen
- Direkte Links zu:
- medisoftware Kundenkartei (Kundennummer)
- Google Maps (Adresse)
- Telefon (Klick zum Anrufen)
- E-Mail (Klick zum Mailen)
- Responsive Design für alle Geräte
- Automatische Aktualisierung der Ergebnisse
- Leere Ergebnisliste bei leeren Suchfeldern
## Installation ## Installation
@@ -36,15 +23,13 @@ cd medi-customers
docker-compose up -d docker-compose up -d
``` ```
3. Die Anwendung ist unter `http://localhost:5001` erreichbar. Die Anwendung ist dann unter `http://localhost:5000` erreichbar.
## Entwicklung ## Versionen
- Python 3.11 - v1.2.0 (2024-03-17): Benutzer-Login hinzugefügt
- Flask - v1.1.0 (2024-03-17): Verbesserte Darstellung der Telefonnummern
- Docker - v1.0.0 (2024-03-17): Erste Version mit grundlegenden Suchfunktionen
- Bootstrap 5
- Font Awesome
## Lizenz ## Lizenz
@@ -96,4 +81,4 @@ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt
## Version ## Version
Aktuelle Version: [1.0.5](CHANGELOG.md#105---2024-03-17) Aktuelle Version: [v1.2.0](CHANGELOG.md#v120---2024-03-17)

42
app.py
View File

@@ -1,4 +1,4 @@
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
@@ -7,8 +7,10 @@ from datetime import datetime, timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
import requests import requests
from collections import defaultdict from collections import defaultdict
import ipaddress
app = Flask(__name__, static_folder='static') app = Flask(__name__, static_folder='static')
app.secret_key = 'your_secret_key' # Setzen Sie einen sicheren geheimen Schlüssel für die Session
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -21,6 +23,9 @@ CSV_FILE = "data/customers.csv"
# Lade Umgebungsvariablen # Lade Umgebungsvariablen
load_dotenv() load_dotenv()
# Statisches Passwort aus der .env Datei
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'changeme')
def clean_dataframe(df): def clean_dataframe(df):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität""" """Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
return df.replace({np.nan: None}) return df.replace({np.nan: None})
@@ -48,12 +53,47 @@ def load_data():
logger.error(f"Fehler beim Laden der CSV-Datei: {str(e)}") logger.error(f"Fehler beim Laden der CSV-Datei: {str(e)}")
return None return None
@app.route('/login', methods=['GET', 'POST'])
def login():
# Versuche, die tatsächliche Client-IP aus dem X-Forwarded-For-Header zu erhalten
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
allowed_ip_ranges = os.getenv('ALLOWED_IP_RANGES', '').split(',')
logger.info(f"Client-IP: {client_ip}")
logger.info(f"Erlaubte IP-Bereiche: {allowed_ip_ranges}")
# Überprüfen, ob die IP-Adresse in einem der erlaubten Subnetze liegt
client_ip_obj = ipaddress.ip_address(client_ip)
for ip_range in allowed_ip_ranges:
try:
network = ipaddress.ip_network(ip_range.strip(), strict=False)
logger.info(f"Überprüfe Netzwerk: {network}")
if client_ip_obj in network:
logger.info("Client-IP ist im erlaubten Bereich.")
session['logged_in'] = True
return redirect(url_for('index'))
except ValueError:
logger.error(f"Ungültiges Netzwerkformat: {ip_range}")
if request.method == 'POST':
password = request.form.get('password')
if password == STATIC_PASSWORD:
session['logged_in'] = True
return redirect(url_for('index'))
else:
return render_template('login.html', error="Falsches Passwort")
return render_template('login.html')
@app.route('/') @app.route('/')
def index(): def index():
if not session.get('logged_in'):
return redirect(url_for('login'))
return render_template('index.html') return render_template('index.html')
@app.route('/search') @app.route('/search')
def search(): def search():
if not session.get('logged_in'):
return redirect(url_for('login'))
try: try:
# CSV-Datei laden # CSV-Datei laden
df = load_data() df = load_data()

View File

@@ -1,5 +1,3 @@
version: '3.8'
services: services:
web: web:
build: . build: .

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -166,60 +166,82 @@
.customer-info { .customer-info {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.footer-content {
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
width: 100%;
position: fixed;
bottom: 0;
left: 0;
z-index: 100;
}
.footer-link {
color: #0d6efd;
text-decoration: none;
}
.footer-link:hover {
text-decoration: underline;
}
</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="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 class="input-group mb-4 position-relative">
<input type="text" id="searchInput" class="form-control form-control-lg"
placeholder="Allgemeine Suche...">
<i class="fa-solid fa-xmark reset-icon" id="searchReset"></i>
<span class="search-icon">🔍</span>
</div> </div>
<div class="search-container">
<h1 class="text-center mb-4">medisoftware Kundensuche</h1>
<div class="input-group mb-4 position-relative">
<input type="text" id="searchInput" class="form-control form-control-lg"
placeholder="Allgemeine Suche...">
<i class="fa-solid fa-xmark reset-icon" id="searchReset"></i>
<span class="search-icon">🔍</span>
</div>
<div class="search-fields"> <div class="search-fields">
<div class="search-field"> <div class="search-field">
<input type="text" id="nameInput" class="form-control" <input type="text" id="nameInput" class="form-control"
placeholder="Name..."> placeholder="Name...">
<i class="fa-solid fa-xmark reset-icon" id="nameReset"></i> <i class="fa-solid fa-xmark reset-icon" id="nameReset"></i>
</div>
<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 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 class="result-counts"> <div class="result-counts">
<span id="generalCount" class="result-count"></span> <span id="generalCount" class="result-count"></span>
</div>
<div id="loading" class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div> </div>
</div>
<div id="results" class="mt-4"> <div id="loading" class="loading">
<!-- Hier werden die Suchergebnisse angezeigt --> <div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
<div id="results" class="mt-4">
<!-- Hier werden die Suchergebnisse angezeigt -->
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -229,7 +251,10 @@
</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">
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.0</div>
</div>
</footer> </footer>
<script> <script>
@@ -303,9 +328,13 @@
</a>`; </a>`;
} }
function createCustomerLink(customerNumber) { function adjustCustomerNumber(number) {
if (!customerNumber) return 'N/A'; return number - 12000;
return `<a href="medisw:openkkbefe/P${customerNumber}?NetGrp=4" class="customer-link">${customerNumber}</a>`; }
function createCustomerLink(number) {
const adjustedNumber = adjustCustomerNumber(number);
return `<a href="medisw:openkkbefe/P${adjustedNumber}?NetGrp=4" class="customer-link">${adjustedNumber}</a>`;
} }
function showCopyFeedback() { function showCopyFeedback() {

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">
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.0</div>
</div>
</footer>
</body>
</html>