8 Commits

5 changed files with 199 additions and 21 deletions

10
.gitignore vendored
View File

@@ -44,16 +44,10 @@ coverage.xml
# Docker
.docker/
docker-compose.yml
# Daten
spezexpo.csv
# Database
*.db
data/customers.db
data/customers.csv
docker-compose.yml
/data/*.csv
data/
# Virtual Environment
develop-eggs/

View File

@@ -5,6 +5,18 @@ 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.15] - 2024-03-20
### Hinzugefügt
- Autovervollständigung für das Ort-Feld
- Neue API-Route für Ortsvorschläge
- 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

View File

@@ -14,7 +14,7 @@ Eine moderne Webanwendung zur Suche und Verwaltung von Kundendaten, die MEDISOFT
## Version
Aktuelle Version: 1.2.13
Aktuelle Version: 1.2.15
## Installation
@@ -110,3 +110,20 @@ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt
## Version
Aktuelle Version: [v1.2.4](CHANGELOG.md#v124---2024-03-19)
## Code-Statistiken
Language|files|blank|comment|code
:-------|-------:|-------:|-------:|-------:
HTML|2|56|0|416
CSS|2|51|1|265
Markdown|2|66|0|236
Python|1|51|103|225
YAML|1|0|0|13
Dockerfile|1|8|9|11
Text|1|0|0|5
--------|--------|--------|--------|--------
SUM:|10|232|113|1171
## Lizenz
Alle Rechte vorbehalten. © 2025 medisoftware

54
app.py
View File

@@ -18,7 +18,7 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Version der Anwendung
VERSION = "1.2.13"
VERSION = "1.2.15"
# Pfad zur Datenbank
DB_FILE = 'data/customers.db'
@@ -348,6 +348,58 @@ def search():
logger.error(f"Fehler bei der Suche: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/api/fachrichtungen')
def get_fachrichtungen():
try:
search_term = request.args.get('q', '').lower()
conn = get_db_connection()
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()]
conn.close()
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()
conn = get_db_connection()
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()]
conn.close()
return jsonify(orte)
except Exception as e:
logger.error(f"Fehler beim Abrufen der Orte: {str(e)}")
return jsonify([])
def init_app(app):
"""Initialisiert die Anwendung mit allen notwendigen Einstellungen."""
with app.app_context():

View File

@@ -107,6 +107,8 @@
<script>
let searchTimeout;
let lastResults = [];
let fachrichtungTimeout;
let ortTimeout;
function createPhoneLink(phone) {
if (!phone) return '';
@@ -268,18 +270,22 @@
</div>
</div>
<div class="card-text">
${createFieldIfValue('Nummer', customer.nummer, createCustomerLink)}
${createFieldIfValue('Nummer', highlightText(customer.nummer, generalSearchTerm), createCustomerLink)}
${createFieldIfValue('Adresse', (customer.strasse && customer.plz && customer.ort) ? true : false,
() => createAddressLink(customer.strasse, customer.plz, customer.ort))}
${createFieldIfValue('Telefon', customer.telefon, createPhoneLink)}
${createFieldIfValue('Mobil', customer.mobil, createPhoneLink)}
${createFieldIfValue('Handy', customer.handy, createPhoneLink)}
${createFieldIfValue('Telefon Firma', customer.tele_firma, createPhoneLink)}
${createFieldIfValue('E-Mail', customer.email, createEmailLink)}
() => createAddressLink(
highlightText(customer.strasse, generalSearchTerm),
highlightText(customer.plz, generalSearchTerm),
highlightText(customer.ort, generalSearchTerm)
))}
${createFieldIfValue('Telefon', highlightText(customer.telefon, generalSearchTerm), createPhoneLink)}
${createFieldIfValue('Mobil', highlightText(customer.mobil, generalSearchTerm), createPhoneLink)}
${createFieldIfValue('Handy', highlightText(customer.handy, generalSearchTerm), createPhoneLink)}
${createFieldIfValue('Telefon Firma', highlightText(customer.tele_firma, generalSearchTerm), createPhoneLink)}
${createFieldIfValue('E-Mail', highlightText(customer.email, generalSearchTerm), createEmailLink)}
${createFieldIfValue('Fachrichtung', highlightText(customer.fachrichtung, generalSearchTerm || fachrichtungSearchTerm))}
${createFieldIfValue('Kontakt 1', customer.kontakt1, createPhoneLink)}
${createFieldIfValue('Kontakt 2', customer.kontakt2, createPhoneLink)}
${createFieldIfValue('Kontakt 3', customer.kontakt3, createPhoneLink)}
${createFieldIfValue('Kontakt 1', highlightText(customer.kontakt1, generalSearchTerm), createPhoneLink)}
${createFieldIfValue('Kontakt 2', highlightText(customer.kontakt2, generalSearchTerm), createPhoneLink)}
${createFieldIfValue('Kontakt 3', highlightText(customer.kontakt3, generalSearchTerm), 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('')}
@@ -403,6 +409,103 @@
searchCustomers();
}
});
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';
}
});
}
document.addEventListener('DOMContentLoaded', function() {
setupFachrichtungAutocomplete();
setupOrtAutocomplete();
});
</script>
</body>
</html>