Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
f626cd52fd | |||
6283ccff6d | |||
3893e148ab | |||
65fbde845e | |||
57d3a05a5a | |||
dfd50d5e78 | |||
73541796f6 | |||
e3e9900482 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -44,16 +44,10 @@ coverage.xml
|
|||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
.docker/
|
.docker/
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
# Daten
|
# Daten
|
||||||
spezexpo.csv
|
data/
|
||||||
|
|
||||||
# Database
|
|
||||||
*.db
|
|
||||||
data/customers.db
|
|
||||||
data/customers.csv
|
|
||||||
docker-compose.yml
|
|
||||||
/data/*.csv
|
|
||||||
|
|
||||||
# Virtual Environment
|
# Virtual Environment
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
|
12
CHANGELOG.md
12
CHANGELOG.md
@@ -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/),
|
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/).
|
||||||
|
|
||||||
|
## [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
|
## [1.2.13] - 2024-03-20
|
||||||
### Fixed
|
### Fixed
|
||||||
- Korrektur der Parameteranzahl in der SQL-Abfrage für die allgemeine Suche
|
- Korrektur der Parameteranzahl in der SQL-Abfrage für die allgemeine Suche
|
||||||
|
21
README.md
21
README.md
@@ -14,7 +14,7 @@ Eine moderne Webanwendung zur Suche und Verwaltung von Kundendaten, die MEDISOFT
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
Aktuelle Version: 1.2.13
|
Aktuelle Version: 1.2.15
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -109,4 +109,21 @@ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
Aktuelle Version: [v1.2.4](CHANGELOG.md#v124---2024-03-19)
|
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
54
app.py
@@ -18,7 +18,7 @@ logging.basicConfig(level=logging.INFO)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Version der Anwendung
|
# Version der Anwendung
|
||||||
VERSION = "1.2.13"
|
VERSION = "1.2.15"
|
||||||
|
|
||||||
# Pfad zur Datenbank
|
# Pfad zur Datenbank
|
||||||
DB_FILE = 'data/customers.db'
|
DB_FILE = 'data/customers.db'
|
||||||
@@ -348,6 +348,58 @@ def search():
|
|||||||
logger.error(f"Fehler bei der Suche: {str(e)}")
|
logger.error(f"Fehler bei der Suche: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
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):
|
def init_app(app):
|
||||||
"""Initialisiert die Anwendung mit allen notwendigen Einstellungen."""
|
"""Initialisiert die Anwendung mit allen notwendigen Einstellungen."""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
@@ -107,6 +107,8 @@
|
|||||||
<script>
|
<script>
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
let lastResults = [];
|
let lastResults = [];
|
||||||
|
let fachrichtungTimeout;
|
||||||
|
let ortTimeout;
|
||||||
|
|
||||||
function createPhoneLink(phone) {
|
function createPhoneLink(phone) {
|
||||||
if (!phone) return '';
|
if (!phone) return '';
|
||||||
@@ -268,18 +270,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-text">
|
<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,
|
${createFieldIfValue('Adresse', (customer.strasse && customer.plz && customer.ort) ? true : false,
|
||||||
() => createAddressLink(customer.strasse, customer.plz, customer.ort))}
|
() => createAddressLink(
|
||||||
${createFieldIfValue('Telefon', customer.telefon, createPhoneLink)}
|
highlightText(customer.strasse, generalSearchTerm),
|
||||||
${createFieldIfValue('Mobil', customer.mobil, createPhoneLink)}
|
highlightText(customer.plz, generalSearchTerm),
|
||||||
${createFieldIfValue('Handy', customer.handy, createPhoneLink)}
|
highlightText(customer.ort, generalSearchTerm)
|
||||||
${createFieldIfValue('Telefon Firma', customer.tele_firma, createPhoneLink)}
|
))}
|
||||||
${createFieldIfValue('E-Mail', customer.email, createEmailLink)}
|
${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('Fachrichtung', highlightText(customer.fachrichtung, generalSearchTerm || fachrichtungSearchTerm))}
|
||||||
${createFieldIfValue('Kontakt 1', customer.kontakt1, createPhoneLink)}
|
${createFieldIfValue('Kontakt 1', highlightText(customer.kontakt1, generalSearchTerm), createPhoneLink)}
|
||||||
${createFieldIfValue('Kontakt 2', customer.kontakt2, createPhoneLink)}
|
${createFieldIfValue('Kontakt 2', highlightText(customer.kontakt2, generalSearchTerm), createPhoneLink)}
|
||||||
${createFieldIfValue('Kontakt 3', customer.kontakt3, createPhoneLink)}
|
${createFieldIfValue('Kontakt 3', highlightText(customer.kontakt3, generalSearchTerm), createPhoneLink)}
|
||||||
${customer.tags && customer.tags.length > 0 ? `
|
${customer.tags && customer.tags.length > 0 ? `
|
||||||
<p class="mb-0"><strong>Tags:</strong>
|
<p class="mb-0"><strong>Tags:</strong>
|
||||||
${customer.tags.map(tag => `<span class="badge bg-primary me-1">${tag}</span>`).join('')}
|
${customer.tags.map(tag => `<span class="badge bg-primary me-1">${tag}</span>`).join('')}
|
||||||
@@ -403,6 +409,103 @@
|
|||||||
searchCustomers();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Reference in New Issue
Block a user