diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd2b0e..89c76d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/), und dieses Projekt adhäriert zu [Semantic Versioning](https://semver.org/lang/de/). +## [1.2.16] - 2024-03-21 +### Geändert +- Verbesserte Darstellung der Suchergebnisse mit rechtsbündigen Aktionen +- Optimierte CSS-Styles für bessere Lesbarkeit und Layout +- JavaScript-Code in separate Datei ausgelagert für bessere Wartbarkeit + ## [1.2.15] - 2024-03-20 ### Hinzugefügt - Autovervollständigung für das Ort-Feld diff --git a/README.md b/README.md index 8c9c6d1..7a50150 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Medi-Customers +# medisoftware Kundensuche -Eine moderne Webanwendung zur Suche und Verwaltung von Kundendaten, die MEDISOFT und MEDICONSULT Daten kombiniert. +Eine einfache und effiziente Kundensuche für medisoftware Kunden. ## Features @@ -14,7 +14,7 @@ Eine moderne Webanwendung zur Suche und Verwaltung von Kundendaten, die MEDISOFT ## Version -Aktuelle Version: 1.2.15 +Aktuelle Version: 1.2.16 ## Installation diff --git a/app.py b/app.py index c3f4ec3..8def4ab 100644 --- a/app.py +++ b/app.py @@ -13,13 +13,12 @@ import sqlite3 from functools import wraps app = Flask(__name__, static_folder='static') -app.secret_key = os.getenv('SECRET_KEY', 'default-secret-key') +app.config['SECRET_KEY'] = os.getenv('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.16' logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Version der Anwendung -VERSION = "1.2.15" - # Pfad zur Datenbank DB_FILE = 'data/customers.db' @@ -28,7 +27,6 @@ load_dotenv() # Statisches Passwort aus der .env Datei STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password') -ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',') def isIPInSubnet(ip, subnet): """Überprüft, ob eine IP-Adresse in einem Subnetz liegt.""" @@ -184,7 +182,7 @@ def login(): 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 ALLOWED_IP_RANGES if range.strip()) + 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") @@ -213,8 +211,8 @@ def index(): client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) logger.info(f"Client-IP: {client_ip}") - logger.info(f"Erlaubte IP-Bereiche: {ALLOWED_IP_RANGES}") - return render_template('index.html', allowed_ip_ranges=','.join(ALLOWED_IP_RANGES), version=VERSION) + 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(): diff --git a/static/css/styles.css b/static/css/styles.css index 7ec6d28..87f9f9d 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -182,18 +182,78 @@ body { .customer-card { background: white; border-radius: 8px; - padding: 1.5rem; - margin-bottom: 1.5rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); - border-bottom: 1px solid #e9ecef; -} - -.customer-card:last-child { - border-bottom: none; -} - -.customer-info { + box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 1rem; + padding: 1rem; +} + +.customer-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; +} + +.customer-name { + margin: 0; + font-size: 1.2rem; + color: #333; +} + +.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; } .footer-content { @@ -272,22 +332,26 @@ body { background-color: #ff9800; } -/* Adress-Links */ -.address-text { - margin-right: 5px; +.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); } -.address-link, .route-link { - color: #666; - text-decoration: none; - margin-left: 5px; - transition: color 0.2s; +.autocomplete-items div { + padding: 8px 12px; + cursor: pointer; + background-color: white; } -.address-link:hover, .route-link:hover { - color: #0d6efd; -} - -.location-pin, .route-pin { - font-size: 1.1em; +.autocomplete-items div:hover { + background-color: #f8f9fa; } \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..0b15319 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,373 @@ +let searchTimeout; +let lastResults = []; +let fachrichtungTimeout; +let ortTimeout; + +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 `${formattedNumber}`; +} + +function createEmailLink(email) { + if (!email) return ''; + return `${email}`; +} + +function highlightText(text, searchTerm) { + if (!searchTerm || !text) return text; + // Escapen von Sonderzeichen im Suchbegriff + const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Erstelle einen regulären Ausdruck ohne Wortgrenzen + const regex = new RegExp(escapedSearchTerm, 'gi'); + return text.replace(regex, '$&'); +} + +function createAddressLink(street, plz, city) { + if (!street || !plz || !city) return ''; + const address = `${street}, ${plz} ${city}`; + const searchQuery = encodeURIComponent(address); + const routeQuery = encodeURIComponent(address); + return `${address} + + + + + + `; +} + +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 `${nummer}`; + } 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() { + // Nur Gesamtzahl anzeigen + const generalCount = lastResults.length; + document.getElementById('resultCount').textContent = + generalCount > 0 ? `${generalCount} Treffer gefunden` : ''; + document.getElementById('resultCount').classList.toggle('visible', generalCount > 0); +} + +function displayResults(results) { + const resultsDiv = document.getElementById('results'); + const resultCount = document.getElementById('resultCount'); + const generalSearchTerm = document.getElementById('q').value; + const nameSearchTerm = document.getElementById('nameInput').value; + const fachrichtungSearchTerm = document.getElementById('fachrichtungInput').value; + + if (!results || results.length === 0) { + resultsDiv.innerHTML = '
Keine Ergebnisse gefunden.
'; + resultCount.textContent = '0 Ergebnisse'; + return; + } + + resultCount.textContent = `${results.length} Ergebnisse`; + lastResults = results; + + const resultsHTML = results.map(customer => { + const highlightedName = highlightText(customer.name, nameSearchTerm); + const highlightedFachrichtung = highlightText(customer.fachrichtung, fachrichtungSearchTerm); + const highlightedGeneral = highlightText(customer.name, generalSearchTerm) || + highlightText(customer.fachrichtung, generalSearchTerm) || + highlightText(customer.ort, generalSearchTerm); + + // 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 `${label}: ${formattedValue}
`; + }; + + return ` +Tags: + ${customer.tags.map(tag => `${tag}`).join('')} +
+ ` : ''} +Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.
'; + } 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(); +}); \ No newline at end of file