368 lines
17 KiB
HTML
368 lines
17 KiB
HTML
<!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 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://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div class="main-content">
|
|
<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">Kundensuche</h1>
|
|
|
|
<div class="general-search mb-4">
|
|
<div class="input-group">
|
|
<input type="text" id="q" class="form-control form-control-lg" placeholder="Allgemeine Suche" oninput="searchCustomers()">
|
|
<i class="fas fa-times reset-icon" onclick="clearInput('q')"></i>
|
|
<i class="fas fa-search search-icon"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-fields">
|
|
<div class="search-field">
|
|
<div class="input-group">
|
|
<input type="text" id="nameInput" class="form-control" placeholder="Name" oninput="searchCustomers()">
|
|
<i class="fas fa-times reset-icon" onclick="clearInput('nameInput')"></i>
|
|
<i class="fas fa-search search-icon"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-field">
|
|
<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>
|
|
|
|
<div class="result-counts">
|
|
<span id="resultCount" 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 id="results"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="shareFeedback" class="share-feedback">
|
|
Link in die Zwischenablage kopiert!
|
|
</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.6</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
let searchTimeout;
|
|
let lastResults = [];
|
|
|
|
function createPhoneLink(phone) {
|
|
if (!phone) return 'N/A';
|
|
|
|
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
|
|
const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
|
|
|
|
// Überprüfen, ob die Client-IP in einem der erlaubten Bereiche liegt
|
|
const isAllowed = allowedIPRanges.some(range => isIPInSubnet(clientIP, range.trim()));
|
|
|
|
// Entferne alle nicht-numerischen Zeichen
|
|
let cleanNumber = phone.replace(/\D/g, '');
|
|
|
|
// Füge eine führende 0 hinzu, wenn isAllowed true ist
|
|
if (isAllowed) {
|
|
cleanNumber = '0' + cleanNumber;
|
|
}
|
|
|
|
// 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 'N/A';
|
|
return `<a href="mailto:${email}" class="email-link">${email}</a>`;
|
|
}
|
|
|
|
function highlightText(text, searchTerm) {
|
|
if (!searchTerm) return text;
|
|
const regex = new RegExp(`(${searchTerm})`, 'gi');
|
|
return text.replace(regex, '<mark>$1</mark>');
|
|
}
|
|
|
|
function createAddressLink(street, plz, city) {
|
|
if (!street || !plz || !city) return 'N/A';
|
|
const address = `${street}, ${plz} ${city}`;
|
|
const searchQuery = encodeURIComponent(address);
|
|
const routeQuery = encodeURIComponent(address);
|
|
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
|
|
return `<span class="address-text">${address}</span>
|
|
<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}"
|
|
class="address-link" target="_blank" rel="noopener noreferrer">
|
|
<i class="fa-solid fa-location-pin 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-car 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 = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
|
|
const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
|
|
|
|
// Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt
|
|
const isAllowed = allowedIPRanges.some(range => {
|
|
const trimmedRange = range.trim();
|
|
return isIPInSubnet(clientIP, trimmedRange);
|
|
});
|
|
|
|
const adjustedNumber = adjustCustomerNumber(nummer);
|
|
if (isAllowed) {
|
|
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() {
|
|
// 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');
|
|
resultsDiv.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
|
|
return;
|
|
}
|
|
|
|
// Hole alle Suchbegriffe
|
|
const searchTerms = {
|
|
general: document.getElementById('q').value,
|
|
name: document.getElementById('nameInput').value,
|
|
ort: document.getElementById('ortInput').value,
|
|
nummer: document.getElementById('nummerInput').value,
|
|
plz: document.getElementById('plzInput').value,
|
|
fachrichtung: document.getElementById('fachrichtungInput').value
|
|
};
|
|
|
|
results.forEach(customer => {
|
|
const card = document.createElement('div');
|
|
card.className = 'customer-card';
|
|
card.innerHTML = `
|
|
<div class="customer-info">
|
|
<h5 class="mb-1">${highlightText(customer.name, searchTerms.general || searchTerms.name)}</h5>
|
|
<p class="mb-1 customer-number">${createCustomerLink(customer.nummer)}</p>
|
|
<p class="mb-1">${createAddressLink(
|
|
customer.strasse,
|
|
highlightText(customer.plz, searchTerms.general || searchTerms.plz),
|
|
highlightText(customer.ort, searchTerms.general || searchTerms.ort)
|
|
)}</p>
|
|
<p class="mb-1">Tel: ${createPhoneLink(customer.telefon)}</p>
|
|
${customer.mobil ? `<p class="mb-1">Mobil: ${createPhoneLink(customer.mobil)}</p>` : ''}
|
|
${customer.email ? `<p class="mb-1">E-Mail: ${createEmailLink(customer.email)}</p>` : ''}
|
|
${customer.bemerkung ? `<p class="mb-1">Bemerkung: ${customer.bemerkung}</p>` : ''}
|
|
${customer.fachrichtung ? `<p class="mb-1">Fachrichtung: ${highlightText(customer.fachrichtung, searchTerms.general || searchTerms.fachrichtung)}</p>` : ''}
|
|
</div>
|
|
<div class="card-actions">
|
|
<button class="share-button" onclick="copyCustomerLink('${customer.nummer}')">
|
|
<i class="fas fa-share-alt"></i> Teilen
|
|
</button>
|
|
</div>
|
|
`;
|
|
resultsDiv.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function searchCustomers() {
|
|
const q = document.getElementById('q').value;
|
|
const name = document.getElementById('nameInput').value;
|
|
const ort = document.getElementById('ortInput').value;
|
|
const nummer = document.getElementById('nummerInput').value;
|
|
const plz = document.getElementById('plzInput').value;
|
|
const fachrichtung = document.getElementById('fachrichtungInput').value;
|
|
|
|
// Zeige das Lade-Icon
|
|
document.getElementById('loading').style.display = 'block';
|
|
|
|
// Baue die Suchanfrage
|
|
const params = new URLSearchParams();
|
|
if (q) params.append('q', q);
|
|
if (name) params.append('name', name);
|
|
if (ort) params.append('ort', ort);
|
|
if (nummer) params.append('nummer', nummer);
|
|
if (plz) params.append('plz', plz);
|
|
if (fachrichtung) params.append('fachrichtung', fachrichtung);
|
|
|
|
// Führe die Suche durch
|
|
fetch('/search?' + params.toString())
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Verstecke das Lade-Icon
|
|
document.getElementById('loading').style.display = 'none';
|
|
|
|
if (data.error) {
|
|
return;
|
|
}
|
|
|
|
lastResults = data;
|
|
updateResultCounts();
|
|
displayResults(data);
|
|
})
|
|
.catch(error => {
|
|
document.getElementById('loading').style.display = 'none';
|
|
});
|
|
}
|
|
|
|
// Event-Listener für die Live-Suche
|
|
const searchInputs = [
|
|
document.getElementById('q'),
|
|
document.getElementById('nameInput'),
|
|
document.getElementById('ortInput'),
|
|
document.getElementById('nummerInput'),
|
|
document.getElementById('plzInput'),
|
|
document.getElementById('fachrichtungInput')
|
|
];
|
|
|
|
const resetIcons = [
|
|
document.querySelector('.reset-icon[onclick="clearInput(\'q\')"]'),
|
|
document.querySelector('.reset-icon[onclick="clearInput(\'nameInput\')"]'),
|
|
document.querySelector('.reset-icon[onclick="clearInput(\'ortInput\')"]'),
|
|
document.querySelector('.reset-icon[onclick="clearInput(\'nummerInput\')"]'),
|
|
document.querySelector('.reset-icon[onclick="clearInput(\'plzInput\')"]'),
|
|
document.querySelector('.reset-icon[onclick="clearInput(\'fachrichtungInput\')"]')
|
|
];
|
|
|
|
searchInputs.forEach((input, index) => {
|
|
input.addEventListener('input', function() {
|
|
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 plz = urlParams.get('plz');
|
|
|
|
if (name) document.getElementById('nameInput').value = name;
|
|
if (ort) document.getElementById('ortInput').value = ort;
|
|
if (kundennummer) document.getElementById('nummerInput').value = kundennummer;
|
|
if (plz) document.getElementById('plzInput').value = plz;
|
|
|
|
if (name || ort || kundennummer || plz) {
|
|
searchCustomers();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |