25 Commits

Author SHA1 Message Date
57d6daa298 Feat: Offline-Funktionalität mit IndexedDB implementiert 2025-03-18 16:22:57 +01:00
80281d1c2c Feat: App-Icons für PWA hinzugefügt 2025-03-18 16:20:31 +01:00
4c69478fa8 Feat: PWA-Implementierung - Offline-Funktionalität hinzugefügt 2025-03-18 16:16:47 +01:00
35645fc671 Fix: Login-Funktionalität wiederhergestellt und Passwort aktualisiert 2025-03-18 16:11:10 +01:00
fab869eb58 Logo mit Link zu medisoftware.de versehen 2025-03-18 15:47:56 +01:00
6cd8f199c4 Gitignore: docker-compose.yml hinzugefügt 2025-03-18 15:42:48 +01:00
948a17b739 Docker Compose: Beispiel-Konfiguration hinzugefügt und Original aus Repository entfernt 2025-03-18 15:41:23 +01:00
f2290cf77f Version 1.2.6: Verbesserte Suchfunktion und Highlighting - app.py aktualisiert 2025-03-18 15:33:01 +01:00
0627b6ff33 Version 1.2.6: Verbesserte Suchfunktion und Highlighting 2025-03-18 15:32:20 +01:00
24ba040537 Version 1.2.5: Dokumentation aktualisiert 2025-03-18 15:12:50 +01:00
997786be54 Feature: Suchfeld für Fachrichtung hinzugefügt 2025-03-18 15:11:15 +01:00
c4974787d4 Dokumentation: Korrigiere Versionsangabe am Ende der README.md 2025-03-18 14:08:04 +01:00
a42bdaa721 Dokumentation: Aktualisiere Versionsnummer in README.md auf v1.2.4 2025-03-18 14:06:31 +01:00
49938a1085 Version 1.2.4: Performance-Optimierung durch Datenbankindizes 2025-03-18 14:02:07 +01:00
d0a27fe095 Performance: Indizes für alle Suchfelder hinzugefügt 2025-03-18 14:01:08 +01:00
d388bce528 Footer Text geändert 2025-03-18 13:59:57 +01:00
aabb4540c9 Version 1.2.3: Performance-Optimierung durch Entfernung von Debug-Ausgaben 2025-03-18 13:57:32 +01:00
ffde078238 Git: Füge data/customers.db zur .gitignore hinzu 2025-03-18 13:55:17 +01:00
9e320c4eb2 Performance: Entferne alle console.log Anweisungen 2025-03-18 13:53:14 +01:00
58ed5fe867 Performance: Entferne Debug-Logging aus createPhoneLink Funktion 2025-03-18 13:50:32 +01:00
72676edc10 Git: Füge data/customers.db zur .gitignore hinzu 2025-03-18 13:48:55 +01:00
d5954eac89 Version 1.2.2: Verbesserte Telefonnummern-Formatierung und Dokumentation 2025-03-18 13:47:41 +01:00
68a2db28a1 Dokumentation: Aktualisierte README.md und CHANGELOG.md für Version 1.2.2 2025-03-18 13:46:33 +01:00
13709de515 Version 1.2.2: Verbesserte Telefonnummern-Formatierung 2025-03-18 13:43:07 +01:00
2c65d5f651 Dokumentation: Aktualisierung für Version 1.2.1 2025-03-18 12:56:15 +01:00
18 changed files with 967 additions and 302 deletions

8
.gitignore vendored
View File

@@ -45,4 +45,10 @@ coverage.xml
.docker/ .docker/
# Daten # Daten
spezexpo.csv spezexpo.csv
# Database
*.db
data/customers.db
data/customers.csv
docker-compose.yml

View File

@@ -5,7 +5,51 @@ 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/).
## [v1.2.0] - 2024-03-18 ## [1.2.6] - 2024-03-19
### Geändert
- Verbesserte Suchfunktion: Keine Ergebnisse mehr bei leeren Suchfeldern
- Optimiertes Highlighting der Suchergebnisse für alle Suchfelder
- Fachrichtung wird jetzt in den Suchergebnissen hervorgehoben
## [1.2.5] - 2024-03-19
### Hinzugefügt
- Neues Suchfeld für Fachrichtung
- Index für das Fachrichtung-Feld in der Datenbank
- Fachrichtung in der allgemeinen Suche integriert
## [1.2.4] - 2024-03-19
### Geändert
- Performance-Optimierung: Indizes für alle Suchfelder hinzugefügt
- Verbesserte Suchgeschwindigkeit durch optimierte Datenbankindizes
- Zusammengesetzter Index für die häufigste Suchkombination (Name + Ort) hinzugefügt
## [1.2.3] - 2024-03-19
### Geändert
- Performance-Optimierung: Entfernung aller console.log Anweisungen
- Verbesserte Code-Qualität durch Entfernung von Debug-Ausgaben
## [1.2.2] - 2024-03-19
### Geändert
- Verbesserte Telefonnummern-Formatierung: Führende "0" wird immer hinzugefügt, wenn der Benutzer von einer erlaubten IP-Adresse zugreift
- Debug-Logging für Telefonnummern-Formatierung hinzugefügt
- Verbesserte Benutzerfreundlichkeit bei der Anzeige von Telefonnummern
### Behoben
- Problem mit fehlender führender "0" bei Telefonnummern für autorisierte Benutzer
## [1.2.1] - 2024-03-18
### Geändert
- Verbesserte CSV-Import-Funktionalität mit pandas
- Korrektur des Login-Prozesses
- Verbesserte Fehlerbehandlung und Logging
- Anpassung der Spaltennamen für den CSV-Import
### Behoben
- Login-Prozess funktioniert jetzt korrekt mit dem Passwort aus der .env Datei
- CSV-Import verarbeitet Anführungszeichen korrekt
- Verbesserte Fehlerbehandlung beim Datenbankimport
## [1.2.0] - 2024-03-18
### Geändert ### Geändert
- IP-Subnetz-Prüfung korrigiert (CIDR-Notation wird jetzt korrekt ausgewertet) - IP-Subnetz-Prüfung korrigiert (CIDR-Notation wird jetzt korrekt ausgewertet)

View File

@@ -10,8 +10,12 @@ COPY requirements.txt .
# Installiere Abhängigkeiten # Installiere Abhängigkeiten
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Kopiere den Anwendungscode # Erstelle das data Verzeichnis und setze Berechtigungen
COPY . . RUN mkdir -p /app/data && \
chmod 755 /app/data
# Kopiere die Anwendungsdateien
COPY . /app/
# Exponiere Port 5000 # Exponiere Port 5000
EXPOSE 5000 EXPOSE 5000

View File

@@ -1,6 +1,6 @@
# medisoftware Kundensuche # Medi-Customers
Eine einfache Webanwendung zur Suche nach medisoftware Kunden mit IP-basierter Zugriffssteuerung. Eine Flask-basierte Webanwendung zur Verwaltung von Kundenkontakten für medizinische Einrichtungen.
## Features ## Features
@@ -51,7 +51,7 @@ Die Anwendung unterstützt CIDR-Notation für IP-Bereiche. Beispiele:
## Version ## Version
Aktuelle Version: v1.2.0 Aktuelle Version: v1.2.6
## Lizenz ## Lizenz
@@ -103,4 +103,4 @@ curl "http://localhost:5001/search?fachrichtung=Zahnarzt&ort=Berlin&name=Schmidt
## Version ## Version
Aktuelle Version: [v1.2.0](CHANGELOG.md#v120---2024-03-17) Aktuelle Version: [v1.2.4](CHANGELOG.md#v124---2024-03-19)

214
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, jsonify, url_for, redirect, session from flask import Flask, render_template, request, jsonify, url_for, redirect, session, make_response, send_from_directory
import pandas as pd import pandas as pd
import os import os
import logging import logging
@@ -21,7 +21,10 @@ logger = logging.getLogger(__name__)
VERSION = "1.2.1" VERSION = "1.2.1"
# Pfad zur CSV-Datei # Pfad zur CSV-Datei
CSV_FILE = "data/customers.csv" CSV_FILE = 'data/customers.csv'
# Pfad zur Datenbank
DB_FILE = 'data/customers.db'
# Lade Umgebungsvariablen # Lade Umgebungsvariablen
load_dotenv() load_dotenv()
@@ -31,11 +34,11 @@ STATIC_PASSWORD = os.getenv('LOGIN_PASSWORD', 'default-password')
ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',') ALLOWED_IP_RANGES = os.getenv('ALLOWED_IP_RANGES', '').split(',')
def init_db(): def init_db():
"""Initialisiert die SQLite-Datenbank und erstellt die notwendigen Tabellen.""" """Initialisiert die SQLite-Datenbank mit der notwendigen Tabelle."""
conn = sqlite3.connect('customers.db') conn = sqlite3.connect(DB_FILE)
c = conn.cursor() c = conn.cursor()
# Erstelle die Kunden-Tabelle # Erstelle die Tabelle
c.execute(''' c.execute('''
CREATE TABLE IF NOT EXISTS customers ( CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -47,16 +50,32 @@ def init_db():
telefon TEXT, telefon TEXT,
mobil TEXT, mobil TEXT,
email TEXT, email TEXT,
bemerkung TEXT bemerkung TEXT,
fachrichtung TEXT
) )
''') ''')
# Erstelle Indizes für alle Suchfelder
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_nummer ON customers(nummer)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_strasse ON customers(strasse)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_plz ON customers(plz)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_ort ON customers(ort)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_telefon ON customers(telefon)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_mobil ON customers(mobil)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_fachrichtung ON customers(fachrichtung)')
# Erstelle einen zusammengesetzten Index für die häufigste Suchkombination
c.execute('CREATE INDEX IF NOT EXISTS idx_customers_name_ort ON customers(name, ort)')
conn.commit() conn.commit()
conn.close() conn.close()
logger.info('Datenbank initialisiert')
def import_csv(): def import_csv():
"""Importiert die Daten aus der CSV-Datei in die SQLite-Datenbank.""" """Importiert die Daten aus der CSV-Datei in die SQLite-Datenbank."""
conn = sqlite3.connect('customers.db') conn = sqlite3.connect(DB_FILE)
c = conn.cursor() c = conn.cursor()
# Lösche bestehende Daten # Lösche bestehende Daten
@@ -64,7 +83,7 @@ def import_csv():
try: try:
# Lese die CSV-Datei mit pandas # Lese die CSV-Datei mit pandas
df = pd.read_csv('data/customers.csv', sep=',', encoding='utf-8', quotechar='"') df = pd.read_csv(CSV_FILE, sep=',', encoding='utf-8', quotechar='"')
# Entferne Anführungszeichen aus den Spaltennamen # Entferne Anführungszeichen aus den Spaltennamen
df.columns = df.columns.str.strip('"') df.columns = df.columns.str.strip('"')
@@ -80,8 +99,8 @@ def import_csv():
# Importiere die Daten # Importiere die Daten
for _, row in df.iterrows(): for _, row in df.iterrows():
c.execute(''' c.execute('''
INSERT INTO customers (nummer, name, strasse, plz, ort, telefon, mobil, email, bemerkung) INSERT INTO customers (nummer, name, strasse, plz, ort, telefon, mobil, email, bemerkung, fachrichtung)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
row['Nummer'], row['Nummer'],
row['name'], row['name'],
@@ -91,7 +110,8 @@ def import_csv():
row['Tel'], row['Tel'],
row['Handy'], row['Handy'],
row['mail'], row['mail'],
f"Fachrichtung: {row['Fachrichtung']}" f"Fachrichtung: {row['Fachrichtung']}",
row['Fachrichtung']
)) ))
conn.commit() conn.commit()
@@ -103,48 +123,96 @@ def import_csv():
conn.close() conn.close()
def search_customers(search_params): def search_customers(search_params):
"""Sucht Kunden in der Datenbank basierend auf den Suchparametern.""" """Sucht nach Kunden basierend auf den Suchparametern."""
conn = sqlite3.connect('customers.db') # Prüfe, ob alle Suchfelder leer sind
if not any([
search_params.get('q'),
search_params.get('name'),
search_params.get('ort'),
search_params.get('nummer'),
search_params.get('plz'),
search_params.get('telefon'),
search_params.get('email'),
search_params.get('fachrichtung')
]):
return []
conn = sqlite3.connect(DB_FILE)
c = conn.cursor() c = conn.cursor()
# Erstelle die WHERE-Bedingungen basierend auf den Suchparametern try:
conditions = [] # Baue die SQL-Abfrage dynamisch auf
params = [] query = "SELECT * FROM customers WHERE 1=1"
params = []
if search_params.get('name'):
conditions.append('name LIKE ?') # Allgemeine Suche über alle Felder
params.append(f'%{search_params["name"]}%') if search_params.get('q'):
search_term = f"%{search_params['q']}%"
if search_params.get('ort'): operator = search_params.get('operator', 'or').upper()
conditions.append('ort LIKE ?')
params.append(f'%{search_params["ort"]}%') if operator == 'AND':
# Bei UND-Verknüpfung müssen alle Begriffe in mindestens einem Feld vorkommen
if search_params.get('nummer'): terms = search_params['q'].split()
conditions.append('nummer LIKE ?') conditions = []
params.append(f'%{search_params["nummer"]}%') for term in terms:
term = f"%{term}%"
if search_params.get('plz'): conditions.append("(name LIKE ? OR ort LIKE ? OR nummer LIKE ? OR telefon LIKE ? OR mobil LIKE ? OR email LIKE ? OR bemerkung LIKE ? OR fachrichtung LIKE ?)")
conditions.append('plz LIKE ?') params.extend([term] * 8)
params.append(f'%{search_params["plz"]}%') query += " AND " + " AND ".join(conditions)
else:
# Erstelle die SQL-Abfrage # Bei ODER-Verknüpfung (Standard) muss mindestens ein Begriff in einem Feld vorkommen
sql = 'SELECT * FROM customers' query += " AND (name LIKE ? OR ort LIKE ? OR nummer LIKE ? OR telefon LIKE ? OR mobil LIKE ? OR email LIKE ? OR bemerkung LIKE ? OR fachrichtung LIKE ?)"
if conditions: params.extend([search_term] * 8)
sql += ' WHERE ' + ' AND '.join(conditions)
# Spezifische Suche für einzelne Felder
# Führe die Abfrage aus if search_params.get('name'):
c.execute(sql, params) query += " AND name LIKE ?"
results = c.fetchall() params.append(f"%{search_params['name']}%")
# Konvertiere die Ergebnisse in ein Dictionary if search_params.get('ort'):
columns = ['id', 'nummer', 'name', 'strasse', 'plz', 'ort', 'telefon', 'mobil', 'email', 'bemerkung'] query += " AND ort LIKE ?"
customers = [] params.append(f"%{search_params['ort']}%")
for row in results:
customer = dict(zip(columns, row)) if search_params.get('nummer'):
customers.append(customer) query += " AND nummer LIKE ?"
params.append(f"%{search_params['nummer']}%")
conn.close()
return customers if search_params.get('plz'):
query += " AND plz LIKE ?"
params.append(f"%{search_params['plz']}%")
if search_params.get('fachrichtung'):
query += " AND fachrichtung LIKE ?"
params.append(f"%{search_params['fachrichtung']}%")
# Führe die Abfrage aus
c.execute(query, params)
results = c.fetchall()
# Formatiere die Ergebnisse
customers = []
for row in results:
customer = {
'id': row[0],
'nummer': row[1],
'name': row[2],
'strasse': row[3],
'plz': row[4],
'ort': row[5],
'telefon': row[6],
'mobil': row[7],
'email': row[8],
'bemerkung': row[9],
'fachrichtung': row[10]
}
customers.append(customer)
return customers
except Exception as e:
logger.error(f"Fehler bei der Kundensuche: {str(e)}")
raise
finally:
conn.close()
def clean_dataframe(df): def clean_dataframe(df):
"""Konvertiert NaN-Werte in None für JSON-Kompatibilität""" """Konvertiert NaN-Werte in None für JSON-Kompatibilität"""
@@ -233,7 +301,12 @@ def search():
'name': request.args.get('name', ''), 'name': request.args.get('name', ''),
'ort': request.args.get('ort', ''), 'ort': request.args.get('ort', ''),
'nummer': request.args.get('nummer', ''), 'nummer': request.args.get('nummer', ''),
'plz': request.args.get('plz', '') 'plz': request.args.get('plz', ''),
'telefon': request.args.get('telefon', ''),
'email': request.args.get('email', ''),
'q': request.args.get('q', ''),
'fachrichtung': request.args.get('fachrichtung', ''),
'operator': request.args.get('operator', 'or')
} }
# Führe die Suche in der Datenbank durch # Führe die Suche in der Datenbank durch
@@ -247,6 +320,43 @@ 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('/sw.js')
def sw():
response = make_response(send_from_directory('static', 'sw.js'))
response.headers['Content-Type'] = 'application/javascript'
return response
@app.route('/offline')
def offline():
return render_template('offline.html')
@app.route('/api/search', methods=['POST'])
def api_search():
"""API-Endpunkt für die Kundensuche"""
try:
search_params = request.get_json()
results = search_customers(search_params)
return jsonify(results)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/customers')
def api_customers():
"""API-Endpunkt für alle Kunden"""
try:
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
c.execute('SELECT * FROM customers')
columns = [description[0] for description in c.description]
results = []
for row in c.fetchall():
customer = dict(zip(columns, row))
results.append(customer)
conn.close()
return jsonify(results)
except Exception as e:
return jsonify({"error": str(e)}), 500
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():

View File

@@ -1,12 +0,0 @@
services:
web:
build: .
ports:
- "5001:5000"
volumes:
- .:/app
environment:
- FLASK_APP=app.py
- FLASK_ENV=development
- FLASK_DEBUG=1
command: flask run --host=0.0.0.0

View File

@@ -0,0 +1,13 @@
services:
web:
build: .
ports:
- "5001:5000"
volumes:
- ./data:/app/data
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
- LOGIN_PASSWORD=changeme
- ALLOWED_IP_RANGES=213.178.68.218/29,192.168.0.0/24,192.168.177.0/24
command: flask run --host=0.0.0.0

39
scripts/generate_icons.py Normal file
View File

@@ -0,0 +1,39 @@
from PIL import Image, ImageDraw, ImageFont
import os
def create_icon(size):
# Erstelle ein neues Bild mit grünem Hintergrund
image = Image.new('RGB', (size, size), '#4CAF50')
draw = ImageDraw.Draw(image)
# Versuche, eine Schriftart zu laden
try:
font = ImageFont.truetype("arial.ttf", size // 3)
except:
font = ImageFont.load_default()
# Text "MEDI" zeichnen
text = "MEDI"
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
x = (size - text_width) // 2
y = (size - text_height) // 2
draw.text((x, y), text, fill='white', font=font)
# Speichere das Icon
output_file = f'static/images/icon-{size}x{size}.png'
image.save(output_file, 'PNG')
print(f'Icon {size}x{size} generiert: {output_file}')
def main():
# Erstelle das Verzeichnis, falls es nicht existiert
os.makedirs('static/images', exist_ok=True)
# Generiere Icons in verschiedenen Größen
sizes = [192, 512]
for size in sizes:
create_icon(size)
if __name__ == '__main__':
main()

View File

@@ -109,21 +109,31 @@ body {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 1rem; margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
} }
.share-button { .share-button {
padding: 5px 10px; padding: 0.5rem 1rem;
border-radius: 15px; border-radius: 20px;
font-size: 0.9em; font-size: 0.9rem;
background-color: #0d6efd; background-color: #0d6efd;
color: white; color: white;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.share-button:hover { .share-button:hover {
background-color: #0b5ed7; background-color: #0b5ed7;
transform: translateY(-1px);
}
.share-button i {
font-size: 1rem;
} }
.search-fields { .search-fields {
@@ -204,4 +214,121 @@ body {
.footer-link:hover { .footer-link:hover {
text-decoration: underline; text-decoration: underline;
}
.general-search {
max-width: 800px;
margin: 0 auto;
}
.general-search .input-group {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.general-search .form-control {
height: 3.5rem;
font-size: 1.2rem;
padding: 0.75rem 1rem;
}
.general-search .search-icon,
.general-search .reset-icon {
font-size: 1.2rem;
padding: 0 1rem;
}
.search-options {
font-size: 0.9em;
color: #666;
}
.search-options .form-check {
margin-right: 1rem;
}
.search-options .form-check-input {
cursor: pointer;
}
.search-options .form-check-label {
cursor: pointer;
user-select: none;
}
/* Offline-Indikator */
.offline-indicator {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #ff9800;
color: white;
padding: 10px 20px;
border-radius: 5px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 1000;
}
.offline-icon {
font-size: 1.2em;
}
/* Synchronisations-Status */
.sync-status {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border-radius: 5px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 1000;
}
.sync-icon {
font-size: 1.2em;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Offline-Modus Styling */
body.offline {
filter: grayscale(20%);
}
body.offline .search-container {
opacity: 0.9;
}
body.offline .footer::after {
content: "Offline-Modus";
display: block;
text-align: center;
color: #ff9800;
font-size: 0.8em;
margin-top: 5px;
}
/* Offline-Fallback für Bilder */
.offline img {
opacity: 0.8;
}
/* Verbesserte Sichtbarkeit im Offline-Modus */
.offline .search-field input {
background-color: rgba(255, 255, 255, 0.9);
}
.offline .result-count {
color: #666;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

25
static/images/icon.svg Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<!-- Hintergrund -->
<rect width="512" height="512" fill="#4CAF50"/>
<!-- MEDI Text -->
<text x="256" y="300"
font-family="Arial, sans-serif"
font-size="200"
font-weight="bold"
fill="white"
text-anchor="middle">
MEDI
</text>
<!-- Herz-Symbol -->
<path d="M256 150
C 256 150, 200 100, 150 100
C 100 100, 50 150, 50 200
C 50 250, 256 350, 256 350
C 256 350, 462 250, 462 200
C 462 150, 412 100, 362 100
C 312 100, 256 150, 256 150"
fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 775 B

144
static/js/db.js Normal file
View File

@@ -0,0 +1,144 @@
// IndexedDB Konfiguration
const DB_NAME = 'mediCustomersDB';
const DB_VERSION = 1;
const STORE_NAME = 'customers';
// Datenbank initialisieren
const initDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => {
console.error('Fehler beim Öffnen der Datenbank:', request.error);
reject(request.error);
};
request.onsuccess = () => {
console.log('Datenbank erfolgreich geöffnet');
resolve(request.result);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'nummer' });
// Indizes für die Suche erstellen
store.createIndex('name', 'name', { unique: false });
store.createIndex('ort', 'ort', { unique: false });
store.createIndex('plz', 'plz', { unique: false });
store.createIndex('fachrichtung', 'fachrichtung', { unique: false });
}
};
});
};
// Kunden in IndexedDB speichern
const saveCustomers = async (customers) => {
const db = await initDB();
const tx = db.transaction(STORE_NAME, 'readwrite');
const store = tx.objectStore(STORE_NAME);
return Promise.all(customers.map(customer => {
return new Promise((resolve, reject) => {
const request = store.put(customer);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}));
};
// Kunden aus IndexedDB suchen
const searchCustomersOffline = async (searchParams) => {
const db = await initDB();
const tx = db.transaction(STORE_NAME, 'readonly');
const store = tx.objectStore(STORE_NAME);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => {
let results = request.result;
// Filtern basierend auf den Suchparametern
if (searchParams.q) {
const searchTerms = searchParams.q.toLowerCase().split(' ');
const operator = searchParams.operator || 'or';
results = results.filter(customer => {
const searchableText = `${customer.name} ${customer.ort} ${customer.nummer} ${customer.plz} ${customer.fachrichtung}`.toLowerCase();
if (operator === 'and') {
return searchTerms.every(term => searchableText.includes(term));
} else {
return searchTerms.some(term => searchableText.includes(term));
}
});
}
// Spezifische Feldsuche
if (searchParams.name) {
results = results.filter(c => c.name.toLowerCase().includes(searchParams.name.toLowerCase()));
}
if (searchParams.ort) {
results = results.filter(c => c.ort.toLowerCase().includes(searchParams.ort.toLowerCase()));
}
if (searchParams.nummer) {
results = results.filter(c => c.nummer.toString().includes(searchParams.nummer));
}
if (searchParams.plz) {
results = results.filter(c => c.plz.includes(searchParams.plz));
}
if (searchParams.fachrichtung) {
results = results.filter(c => c.fachrichtung.toLowerCase().includes(searchParams.fachrichtung.toLowerCase()));
}
resolve(results);
};
request.onerror = () => {
reject(request.error);
};
});
};
// Synchronisationsstatus speichern
const syncStatus = {
lastSync: null,
isOnline: navigator.onLine
};
// Event Listener für Online/Offline Status
window.addEventListener('online', () => {
syncStatus.isOnline = true;
document.body.classList.remove('offline');
synchronizeData();
});
window.addEventListener('offline', () => {
syncStatus.isOnline = false;
document.body.classList.add('offline');
});
// Daten mit dem Server synchronisieren
const synchronizeData = async () => {
if (!syncStatus.isOnline) return;
try {
const response = await fetch('/api/customers');
const customers = await response.json();
await saveCustomers(customers);
syncStatus.lastSync = new Date();
console.log('Daten erfolgreich synchronisiert');
} catch (error) {
console.error('Fehler bei der Synchronisation:', error);
}
};
// Export der Funktionen
window.dbHelper = {
initDB,
saveCustomers,
searchCustomersOffline,
synchronizeData,
syncStatus
};

28
static/manifest.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "MEDI Kunden",
"short_name": "MEDI",
"description": "MEDI Kundenverwaltung - Offline-fähige PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4CAF50",
"orientation": "portrait",
"icons": [
{
"src": "/static/images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/static/images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["business", "productivity"],
"lang": "de-DE",
"dir": "ltr",
"prefer_related_applications": false
}

75
static/sw.js Normal file
View File

@@ -0,0 +1,75 @@
const CACHE_NAME = 'medi-customers-v1';
const urlsToCache = [
'/',
'/static/css/styles.css',
'/static/js/script.js',
'/static/images/logo.png',
'/static/images/icon-192x192.png',
'/static/images/icon-512x512.png'
];
// Installation des Service Workers
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Cache geöffnet');
return cache.addAll(urlsToCache);
})
);
});
// Aktivierung des Service Workers
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('Lösche alten Cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
// Fetch-Event-Handler
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache-Treffer - gib die Antwort zurück
if (response) {
return response;
}
// Kein Cache-Treffer - führe Netzwerkanfrage durch
return fetch(event.request)
.then(response => {
// Prüfe, ob wir eine gültige Antwort erhalten haben
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Klone die Antwort
const responseToCache = response.clone();
// Speichere die Antwort im Cache
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// Fallback für Offline-Zugriff
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});

View File

@@ -5,56 +5,88 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>medisoftware Kundensuche</title> <title>medisoftware Kundensuche</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<meta name="theme-color" content="#4CAF50">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="MEDI">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/icon-192x192.png') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <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="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"> <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
<script src="{{ url_for('static', filename='js/db.js') }}"></script>
</head> </head>
<body> <body>
<div class="main-content"> <div class="main-content">
<div class="container"> <div class="container">
<div class="text-center mb-4"> <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;"> <a href="https://medisoftware.de" target="_blank" rel="noopener noreferrer"><img src="{{ url_for('static', filename='medisoftware_logo_rb_200.png') }}" alt="medisoftware Logo" class="img-fluid" style="max-width: 200px;"></a>
</div> </div>
<div class="search-container"> <div class="search-container">
<h1 class="text-center mb-4">medisoftware Kundensuche</h1> <h1 class="text-center mb-4">Kundensuche</h1>
<div class="input-group mb-4 position-relative"> <div class="general-search mb-4">
<input type="text" id="searchInput" class="form-control form-control-lg" <div class="input-group">
placeholder="Allgemeine Suche..."> <input type="text" id="q" class="form-control form-control-lg" placeholder="Allgemeine Suche" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="searchReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('q')"></i>
<span class="search-icon">🔍</span> <i class="fas fa-search search-icon"></i>
</div>
<div class="search-options mt-2">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="searchOperator" id="searchOr" value="or" checked>
<label class="form-check-label" for="searchOr">ODER</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="searchOperator" id="searchAnd" value="and">
<label class="form-check-label" for="searchAnd">UND</label>
</div>
</div>
</div> </div>
<div class="search-fields"> <div class="search-fields">
<div class="search-field"> <div class="search-field">
<input type="text" id="nameInput" class="form-control" <div class="input-group">
placeholder="Name..."> <input type="text" id="nameInput" class="form-control" placeholder="Name" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="nameReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('nameInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="ortInput" class="form-control" <div class="input-group">
placeholder="Ort..."> <input type="text" id="ortInput" class="form-control" placeholder="Ort" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="ortReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('ortInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="kundennummerInput" class="form-control" <div class="input-group">
placeholder="Kundennummer..."> <input type="text" id="nummerInput" class="form-control" placeholder="Kundennummer" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="kundennummerReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('nummerInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="fachrichtungInput" class="form-control" <div class="input-group">
placeholder="Fachrichtung..."> <input type="text" id="plzInput" class="form-control" placeholder="PLZ" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="fachrichtungReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('plzInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
<div class="search-field"> <div class="search-field">
<input type="text" id="telefonInput" class="form-control" <div class="input-group">
placeholder="Telefon..."> <input type="text" id="fachrichtungInput" class="form-control" placeholder="Fachrichtung" oninput="searchCustomers()">
<i class="fa-solid fa-xmark reset-icon" id="telefonReset"></i> <i class="fas fa-times reset-icon" onclick="clearInput('fachrichtungInput')"></i>
<i class="fas fa-search search-icon"></i>
</div>
</div> </div>
</div> </div>
<div class="result-counts"> <div class="result-counts">
<span id="generalCount" class="result-count"></span> <span id="resultCount" class="result-count"></span>
</div> </div>
<div id="loading" class="loading"> <div id="loading" class="loading">
@@ -63,39 +95,96 @@
</div> </div>
</div> </div>
<div id="results" class="mt-4"> <div id="results"></div>
<!-- Hier werden die Suchergebnisse angezeigt -->
</div>
</div> </div>
</div> </div>
</div> </div>
<div id="shareFeedback" class="share-feedback"> <div id="shareFeedback" class="share-feedback">
Link kopiert! Link in die Zwischenablage kopiert!
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="footer-content"> <div class="footer-content">
Made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a> 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.0</div> <div style="font-size: 0.8em;">Version: v1.2.6</div>
</div> </div>
</footer> </footer>
<div id="syncStatus" class="sync-status" style="display: none;">
<span class="sync-icon">🔄</span>
<span class="sync-text">Synchronisiere Daten...</span>
</div>
<div id="offlineIndicator" class="offline-indicator" style="display: none;">
<span class="offline-icon">📡</span>
<span class="offline-text">Offline-Modus</span>
</div>
<script> <script>
// Service Worker und IndexedDB initialisieren
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('ServiceWorker registration successful');
// IndexedDB initialisieren und erste Synchronisation starten
await window.dbHelper.initDB();
await window.dbHelper.synchronizeData();
} catch (err) {
console.log('ServiceWorker registration failed: ', err);
}
});
}
// Offline-Status-Anzeige aktualisieren
const updateOfflineStatus = () => {
const offlineIndicator = document.getElementById('offlineIndicator');
const syncStatus = document.getElementById('syncStatus');
if (!navigator.onLine) {
offlineIndicator.style.display = 'block';
syncStatus.style.display = 'none';
} else {
offlineIndicator.style.display = 'none';
}
};
window.addEventListener('online', updateOfflineStatus);
window.addEventListener('offline', updateOfflineStatus);
updateOfflineStatus();
let searchTimeout; let searchTimeout;
let lastResults = []; let lastResults = [];
function createPhoneLink(phone) { function createPhoneLink(phone) {
if (!phone) return 'N/A'; if (!phone) return 'N/A';
const cleaned = phone.replace(/[^\d+\s]/g, '');
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}'; const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(','); const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
// Überprüfen, ob die Client-IP in einem der erlaubten Bereiche liegt // Überprüfen, ob die Client-IP in einem der erlaubten Bereiche liegt
const isAllowed = allowedIPRanges.some(range => clientIP.startsWith(range.trim())); const isAllowed = allowedIPRanges.some(range => isIPInSubnet(clientIP, range.trim()));
const telLink = cleaned.startsWith('+') ? cleaned : (isAllowed ? '0' + cleaned.replace(/\s/g, '') : cleaned.replace(/\s/g, '')); // Entferne alle nicht-numerischen Zeichen
return `<a href="tel:${telLink}" class="phone-link">${phone}</a>`; 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) { function createEmailLink(email) {
@@ -103,48 +192,10 @@
return `<a href="mailto:${email}" class="email-link">${email}</a>`; return `<a href="mailto:${email}" class="email-link">${email}</a>`;
} }
function highlightText(text, searchTerms) { function highlightText(text, searchTerm) {
// Konvertiere text zu String und prüfe auf null/undefined if (!searchTerm) return text;
const textStr = String(text || ''); const regex = new RegExp(`(${searchTerm})`, 'gi');
if (!textStr || !searchTerms || searchTerms.length === 0) return textStr; return text.replace(regex, '<mark>$1</mark>');
// Escapen der Suchbegriffe für reguläre Ausdrücke
const escapedTerms = searchTerms.map(term =>
String(term || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
).filter(term => term.length > 0);
if (escapedTerms.length === 0) return textStr;
// Erstelle einen temporären div-Element
const tempDiv = document.createElement('div');
tempDiv.innerHTML = textStr;
// Funktion zum Hervorheben von Text
function highlightNode(node) {
if (node.nodeType === 3) { // Text node
const text = node.textContent;
let newText = text;
escapedTerms.forEach(term => {
const regex = new RegExp(`(${term})`, 'gi');
newText = newText.replace(regex, '<mark>$1</mark>');
});
if (newText !== text) {
const span = document.createElement('span');
span.innerHTML = newText;
node.parentNode.replaceChild(span, node);
}
} else if (node.nodeType === 1) { // Element node
// Überspringe mark-Tags und Links
if (node.tagName !== 'MARK' && node.tagName !== 'A') {
Array.from(node.childNodes).forEach(highlightNode);
}
}
}
highlightNode(tempDiv);
return tempDiv.innerHTML;
} }
function createAddressLink(street, plz, city) { function createAddressLink(street, plz, city) {
@@ -153,7 +204,6 @@
const searchQuery = encodeURIComponent(address); const searchQuery = encodeURIComponent(address);
const routeQuery = encodeURIComponent(address); const routeQuery = encodeURIComponent(address);
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}'; const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
console.log('Client IP in createAddressLink:', clientIP);
return `<span class="address-text">${address}</span> return `<span class="address-text">${address}</span>
<a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}" <a href="https://www.google.com/maps/search/?api=1&query=${searchQuery}"
class="address-link" target="_blank" rel="noopener noreferrer"> class="address-link" target="_blank" rel="noopener noreferrer">
@@ -190,19 +240,12 @@
const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}'; const clientIP = '{{ request.headers.get("X-Forwarded-For", request.remote_addr) }}';
const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(','); const allowedIPRanges = '{{ allowed_ip_ranges }}'.split(',');
// Debug-Ausgabe für die IP-Bereiche
console.log('Client IP in createCustomerLink:', clientIP);
console.log('Allowed IP Ranges:', allowedIPRanges);
// Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt // Überprüfe, ob die Client-IP in einem der erlaubten Bereiche liegt
const isAllowed = allowedIPRanges.some(range => { const isAllowed = allowedIPRanges.some(range => {
const trimmedRange = range.trim(); const trimmedRange = range.trim();
console.log('Checking range:', trimmedRange);
return isIPInSubnet(clientIP, trimmedRange); return isIPInSubnet(clientIP, trimmedRange);
}); });
console.log('isAllowed in createCustomerLink:', isAllowed);
const adjustedNumber = adjustCustomerNumber(nummer); const adjustedNumber = adjustCustomerNumber(nummer);
if (isAllowed) { if (isAllowed) {
return `<a href="medisw:openkkbefe/P${adjustedNumber}?NetGrp=4" class="customer-link">${nummer}</a>`; return `<a href="medisw:openkkbefe/P${adjustedNumber}?NetGrp=4" class="customer-link">${nummer}</a>`;
@@ -229,161 +272,126 @@
await navigator.clipboard.writeText(url.toString()); await navigator.clipboard.writeText(url.toString());
showCopyFeedback(); showCopyFeedback();
} catch (err) { } catch (err) {
console.error('Fehler beim Kopieren:', err); // Fehlerbehandlung ohne console.log
} }
} }
function updateResultCounts() { function updateResultCounts() {
// Nur Gesamtzahl anzeigen // Nur Gesamtzahl anzeigen
const generalCount = lastResults.length; const generalCount = lastResults.length;
document.getElementById('generalCount').textContent = document.getElementById('resultCount').textContent =
generalCount > 0 ? `${generalCount} Treffer gefunden` : ''; generalCount > 0 ? `${generalCount} Treffer gefunden` : '';
document.getElementById('generalCount').classList.toggle('visible', generalCount > 0); document.getElementById('resultCount').classList.toggle('visible', generalCount > 0);
} }
function searchCustomers() { function displayResults(results) {
const query = document.getElementById('searchInput').value.trim(); const resultsDiv = document.getElementById('results');
const fachrichtung = document.getElementById('fachrichtungInput').value.trim(); resultsDiv.innerHTML = '';
const ort = document.getElementById('ortInput').value.trim();
const name = document.getElementById('nameInput').value.trim();
const telefon = document.getElementById('telefonInput').value.trim();
const kundennummer = document.getElementById('kundennummerInput')?.value.trim() || '';
// Sammle alle nicht-leeren Suchbegriffe if (results.length === 0) {
const searchTerms = [query, fachrichtung, ort, name, telefon, kundennummer] resultsDiv.innerHTML = '<p>Keine Ergebnisse gefunden.</p>';
.filter(term => term && term.length > 0);
// Prüfe, ob alle Suchfelder leer sind
if (searchTerms.length === 0) {
const resultsDiv = document.getElementById('results');
const generalCount = document.getElementById('generalCount');
resultsDiv.innerHTML = '';
generalCount.textContent = '';
generalCount.classList.remove('visible');
return; return;
} }
const resultsDiv = document.getElementById('results'); // Hole alle Suchbegriffe
const loadingDiv = document.getElementById('loading'); const searchTerms = {
loadingDiv.style.display = 'block'; general: document.getElementById('q').value,
resultsDiv.innerHTML = ''; 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
};
const searchParams = new URLSearchParams(); results.forEach(customer => {
if (query) searchParams.append('q', query); const card = document.createElement('div');
if (fachrichtung) searchParams.append('fachrichtung', fachrichtung); card.className = 'customer-card';
if (ort) searchParams.append('ort', ort); card.innerHTML = `
if (name) searchParams.append('name', name); <div class="customer-info">
if (telefon) searchParams.append('telefon', telefon); <h5 class="mb-1">${highlightText(customer.name, searchTerms.general || searchTerms.name)}</h5>
if (kundennummer) searchParams.append('kundennummer', kundennummer); <p class="mb-1 customer-number">${createCustomerLink(customer.nummer)}</p>
<p class="mb-1">${createAddressLink(
fetch(`/search?${searchParams.toString()}`) customer.strasse,
.then(response => response.json()) highlightText(customer.plz, searchTerms.general || searchTerms.plz),
.then(data => { highlightText(customer.ort, searchTerms.general || searchTerms.ort)
resultsDiv.innerHTML = ''; )}</p>
<p class="mb-1">Tel: ${createPhoneLink(customer.telefon)}</p>
// Prüfe, ob data ein Objekt mit results-Array ist ${customer.mobil ? `<p class="mb-1">Mobil: ${createPhoneLink(customer.mobil)}</p>` : ''}
if (!data || !data.results || !Array.isArray(data.results)) { ${customer.email ? `<p class="mb-1">E-Mail: ${createEmailLink(customer.email)}</p>` : ''}
console.error('Unerwartetes Datenformat:', data); ${customer.bemerkung ? `<p class="mb-1">Bemerkung: ${customer.bemerkung}</p>` : ''}
resultsDiv.innerHTML = '<div class="error">Unerwartetes Datenformat vom Server</div>'; ${customer.fachrichtung ? `<p class="mb-1">Fachrichtung: ${highlightText(customer.fachrichtung, searchTerms.general || searchTerms.fachrichtung)}</p>` : ''}
return; </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);
});
}
const results = data.results; async function searchCustomers() {
if (results.length === 0) { const searchParams = {
resultsDiv.innerHTML = '<div class="no-results">Keine Ergebnisse gefunden</div>'; q: document.getElementById('q').value,
} else { name: document.getElementById('nameInput').value,
results.forEach(customer => { ort: document.getElementById('ortInput').value,
const card = document.createElement('div'); nummer: document.getElementById('nummerInput').value,
card.className = 'customer-card'; plz: document.getElementById('plzInput').value,
fachrichtung: document.getElementById('fachrichtungInput').value,
// Debug-Ausgabe für die Kundendaten operator: document.querySelector('input[name="searchOperator"]:checked').value
console.log('Kundendaten:', customer); };
console.log('Alle verfügbaren Felder:', Object.keys(customer));
console.log('Telefon-bezogene Felder:', { document.getElementById('loading').style.display = 'block';
Telefon: customer.Telefon,
Telefonnummer: customer.Telefonnummer, try {
telefon: customer.telefon, let results;
telefonnummer: customer.telefonnummer, if (navigator.onLine) {
phone: customer.phone, // Online: Server-Suche
'phone.number': customer.phone?.number const response = await fetch('/api/search', {
}); method: 'POST',
headers: {
// Erstelle die Adresse mit Hervorhebung 'Content-Type': 'application/json',
const address = `${customer.Strasse || ''}, ${customer.PLZ || ''} ${customer.Ort || ''}`; },
const addressLink = createAddressLink(customer.Strasse, customer.PLZ, customer.Ort); body: JSON.stringify(searchParams)
const highlightedAddress = highlightText(addressLink, searchTerms); });
results = await response.json();
// Erstelle die Kundennummer mit Hervorhebung
const highlightedNumber = highlightText(customer.Nummer, searchTerms); // Speichere die Ergebnisse in IndexedDB
const customerLink = createCustomerLink(customer.Nummer); await window.dbHelper.saveCustomers(results);
} else {
// Erstelle die Telefonnummern mit Hervorhebung // Offline: Lokale Suche
let phoneNumber = ''; results = await window.dbHelper.searchCustomersOffline(searchParams);
let companyPhone = ''; }
let mobilePhone = '';
lastResults = results;
if (typeof customer === 'object') { displayResults(results);
phoneNumber = customer.Tel || ''; updateResultCounts();
companyPhone = customer['Tele Firma'] || ''; } catch (error) {
mobilePhone = customer.Handy || ''; console.error('Fehler bei der Suche:', error);
} document.getElementById('results').innerHTML = '<p>Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</p>';
} finally {
const phoneLink = createPhoneLink(phoneNumber); document.getElementById('loading').style.display = 'none';
const companyPhoneLink = createPhoneLink(companyPhone); }
const mobilePhoneLink = createPhoneLink(mobilePhone);
const highlightedPhone = highlightText(phoneLink, searchTerms);
const highlightedCompanyPhone = highlightText(companyPhoneLink, searchTerms);
const highlightedMobilePhone = highlightText(mobilePhoneLink, searchTerms);
card.innerHTML = `
<div class="customer-info">
<strong>Kundennummer:</strong> ${customerLink}<br>
<strong>Name:</strong> ${highlightText(`${customer.Vorname || ''} ${customer.Nachname || ''}`, searchTerms)}<br>
<strong>Fachrichtung:</strong> ${highlightText(customer.Fachrichtung || '', searchTerms)}<br>
<strong>Adresse:</strong> ${highlightedAddress}<br>
<strong>Telefon:</strong> ${highlightedPhone}<br>
<strong>Firma:</strong> ${highlightedCompanyPhone}<br>
<strong>Mobil:</strong> ${highlightedMobilePhone}
</div>
<button class="share-button" onclick="copyCustomerLink('${adjustCustomerNumber(customer.Nummer)}')">
<i class="fas fa-share-alt"></i> Teilen
</button>
`;
resultsDiv.appendChild(card);
});
}
// Aktualisiere die Anzahl der Treffer
const generalCount = document.getElementById('generalCount');
generalCount.textContent = results.length > 0 ? `${results.length} Treffer gefunden` : '';
generalCount.classList.toggle('visible', results.length > 0);
})
.catch(error => {
console.error('Fehler bei der Suche:', error);
resultsDiv.innerHTML = '<div class="error">Ein Fehler ist aufgetreten</div>';
})
.finally(() => {
loadingDiv.style.display = 'none';
});
} }
// Event-Listener für die Live-Suche // Event-Listener für die Live-Suche
const searchInputs = [ const searchInputs = [
document.getElementById('q'),
document.getElementById('nameInput'), document.getElementById('nameInput'),
document.getElementById('ortInput'), document.getElementById('ortInput'),
document.getElementById('kundennummerInput'), document.getElementById('nummerInput'),
document.getElementById('fachrichtungInput'), document.getElementById('plzInput'),
document.getElementById('telefonInput'), document.getElementById('fachrichtungInput')
document.getElementById('searchInput')
]; ];
const resetIcons = [ const resetIcons = [
document.getElementById('nameReset'), document.querySelector('.reset-icon[onclick="clearInput(\'q\')"]'),
document.getElementById('ortReset'), document.querySelector('.reset-icon[onclick="clearInput(\'nameInput\')"]'),
document.getElementById('kundennummerReset'), document.querySelector('.reset-icon[onclick="clearInput(\'ortInput\')"]'),
document.getElementById('fachrichtungReset'), document.querySelector('.reset-icon[onclick="clearInput(\'nummerInput\')"]'),
document.getElementById('telefonReset'), document.querySelector('.reset-icon[onclick="clearInput(\'plzInput\')"]'),
document.getElementById('searchReset') document.querySelector('.reset-icon[onclick="clearInput(\'fachrichtungInput\')"]')
]; ];
searchInputs.forEach((input, index) => { searchInputs.forEach((input, index) => {
@@ -408,18 +416,14 @@
const name = urlParams.get('name'); const name = urlParams.get('name');
const ort = urlParams.get('ort'); const ort = urlParams.get('ort');
const kundennummer = urlParams.get('kundennummer'); const kundennummer = urlParams.get('kundennummer');
const fachrichtung = urlParams.get('fachrichtung'); const plz = urlParams.get('plz');
const telefon = urlParams.get('telefon');
const query = urlParams.get('q');
if (name) document.getElementById('nameInput').value = name; if (name) document.getElementById('nameInput').value = name;
if (ort) document.getElementById('ortInput').value = ort; if (ort) document.getElementById('ortInput').value = ort;
if (kundennummer) document.getElementById('kundennummerInput').value = kundennummer; if (kundennummer) document.getElementById('nummerInput').value = kundennummer;
if (fachrichtung) document.getElementById('fachrichtungInput').value = fachrichtung; if (plz) document.getElementById('plzInput').value = plz;
if (telefon) document.getElementById('telefonInput').value = telefon;
if (query) document.getElementById('searchInput').value = query;
if (name || ort || kundennummer || fachrichtung || telefon || query) { if (name || ort || kundennummer || plz) {
searchCustomers(); searchCustomers();
} }
}); });

View File

@@ -52,8 +52,8 @@
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="footer-content"> <div class="footer-content">
Made with ❤️ and 🍺 by <a href="https://www.medisoftware.de" target="_blank" class="footer-link">medisoftware</a> 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.0</div> <div style="font-size: 0.8em;">Version: v1.2.3</div>
</div> </div>
</footer> </footer>
</body> </body>

58
templates/offline.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - MEDI Kunden</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<style>
.offline-container {
text-align: center;
padding: 2rem;
max-width: 600px;
margin: 2rem auto;
}
.offline-icon {
font-size: 4rem;
color: #666;
margin-bottom: 1rem;
}
.offline-message {
font-size: 1.2rem;
color: #333;
margin-bottom: 1.5rem;
}
.retry-button {
background-color: #4CAF50;
color: white;
padding: 0.8rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.retry-button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="offline-container">
<div class="offline-icon">📡</div>
<h1>Sie sind offline</h1>
<p class="offline-message">
Es scheint, dass Sie keine Internetverbindung haben.
Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.
</p>
<button class="retry-button" onclick="window.location.reload()">
Erneut versuchen
</button>
</div>
<script>
// Automatische Überprüfung der Verbindung
window.addEventListener('online', function() {
window.location.reload();
});
</script>
</body>
</html>