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/),
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
### Geändert
- 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
- Caching für Wetterdaten
## [1.0.0] - 2024-03-17
## [v1.0.0] - 2024-03-17
### Hinzugefügt
- Erste Version der Kundensuche
- Grundlegende Suchfunktionen
- Responsive Design
- Responsive Design
- Docker-Integration

View File

@@ -1,27 +1,14 @@
# 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
- Schnelle und präzise Kundensuche
- Mehrere Suchfelder für gezielte Suche:
- Name (Vor- und Nachname)
- Ort
- Kundennummer
- 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
- Echtzeit-Suche über Kundendaten
- Hervorhebung von Suchbegriffen in den Ergebnissen
- Klickbare Links für Telefonnummern, E-Mail-Adressen und Adressen
- Responsive Design für mobile Geräte
- Docker-Container für einfache Installation und Deployment
## Installation
@@ -36,15 +23,13 @@ cd medi-customers
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
- Flask
- Docker
- Bootstrap 5
- Font Awesome
- v1.2.0 (2024-03-17): Benutzer-Login hinzugefügt
- v1.1.0 (2024-03-17): Verbesserte Darstellung der Telefonnummern
- v1.0.0 (2024-03-17): Erste Version mit grundlegenden Suchfunktionen
## Lizenz
@@ -96,4 +81,4 @@ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt
## 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 os
import logging
@@ -7,8 +7,10 @@ from datetime import datetime, timedelta
from dotenv import load_dotenv
import requests
from collections import defaultdict
import ipaddress
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)
logger = logging.getLogger(__name__)
@@ -21,6 +23,9 @@ CSV_FILE = "data/customers.csv"
# Lade Umgebungsvariablen
load_dotenv()
# Statisches Passwort aus der .env Datei
STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'changeme')
def clean_dataframe(df):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
return df.replace({np.nan: None})
@@ -48,12 +53,47 @@ def load_data():
logger.error(f"Fehler beim Laden der CSV-Datei: {str(e)}")
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('/')
def index():
if not session.get('logged_in'):
return redirect(url_for('login'))
return render_template('index.html')
@app.route('/search')
def search():
if not session.get('logged_in'):
return redirect(url_for('login'))
try:
# CSV-Datei laden
df = load_data()

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

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