10 Commits

15 changed files with 676 additions and 79 deletions

4
.gitignore vendored
View File

@@ -49,4 +49,6 @@ spezexpo.csv
# Database
*.db
data/customers.db
data/customers.db
data/customers.csv
docker-compose.yml

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.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

View File

@@ -51,7 +51,7 @@ Die Anwendung unterstützt CIDR-Notation für IP-Bereiche. Beispiele:
## Version
Aktuelle Version: 1.2.4
Aktuelle Version: v1.2.6
## Lizenz

78
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 os
import logging
@@ -38,7 +38,7 @@ def init_db():
conn = sqlite3.connect(DB_FILE)
c = conn.cursor()
# Erstelle die Tabelle mit Indizes
# Erstelle die Tabelle
c.execute('''
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -124,6 +124,19 @@ def import_csv():
def search_customers(search_params):
"""Sucht nach Kunden basierend auf den Suchparametern."""
# 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()
@@ -135,8 +148,21 @@ def search_customers(search_params):
# Allgemeine Suche über alle Felder
if search_params.get('q'):
search_term = f"%{search_params['q']}%"
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 ?)"
params.extend([search_term] * 8)
operator = search_params.get('operator', 'or').upper()
if operator == 'AND':
# Bei UND-Verknüpfung müssen alle Begriffe in mindestens einem Feld vorkommen
terms = search_params['q'].split()
conditions = []
for term in terms:
term = f"%{term}%"
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 ?)")
params.extend([term] * 8)
query += " AND " + " AND ".join(conditions)
else:
# Bei ODER-Verknüpfung (Standard) muss mindestens ein Begriff in einem Feld vorkommen
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 ?)"
params.extend([search_term] * 8)
# Spezifische Suche für einzelne Felder
if search_params.get('name'):
@@ -154,6 +180,10 @@ def search_customers(search_params):
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)
@@ -275,7 +305,8 @@ def search():
'telefon': request.args.get('telefon', ''),
'email': request.args.get('email', ''),
'q': request.args.get('q', ''),
'fachrichtung': request.args.get('fachrichtung', '')
'fachrichtung': request.args.get('fachrichtung', ''),
'operator': request.args.get('operator', 'or')
}
# Führe die Suche in der Datenbank durch
@@ -289,6 +320,43 @@ def search():
logger.error(f'Fehler bei der Suche: {str(e)}')
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):
"""Initialisiert die Anwendung mit allen notwendigen Einstellungen."""
with app.app_context():

View File

@@ -8,7 +8,6 @@ services:
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
- SECRET_KEY=your-super-secret-key-here
- 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

@@ -235,4 +235,100 @@ body {
.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,15 +5,22 @@
<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 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://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">
<script src="{{ url_for('static', filename='js/db.js') }}"></script>
</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;">
<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 class="search-container">
<h1 class="text-center mb-4">Kundensuche</h1>
@@ -24,6 +31,16 @@
<i class="fas fa-times reset-icon" onclick="clearInput('q')"></i>
<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 class="search-fields">
@@ -59,9 +76,12 @@
</div>
</div>
<div class="form-group">
<label for="fachrichtungInput">Fachrichtung</label>
<input type="text" class="form-control" id="fachrichtungInput" placeholder="Fachrichtung eingeben">
<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>
@@ -87,11 +107,54 @@
<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.2</div>
<div style="font-size: 0.8em;">Version: v1.2.6</div>
</div>
</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>
// 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 lastResults = [];
@@ -230,20 +293,33 @@
return;
}
const searchTerm = document.getElementById('q').value;
// 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, searchTerm)}</h5>
<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, customer.plz, customer.ort)}</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}')">
@@ -255,73 +331,48 @@
});
}
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;
async function searchCustomers() {
const searchParams = {
q: 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,
operator: document.querySelector('input[name="searchOperator"]:checked').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();
const resultsDiv = document.getElementById('results');
resultsDiv.innerHTML = '';
if (data.length === 0) {
resultsDiv.innerHTML = '<p class="text-center text-muted">Keine Ergebnisse gefunden</p>';
return;
}
data.forEach(customer => {
const card = document.createElement('div');
card.className = 'customer-card';
card.innerHTML = `
<div class="customer-info">
<h5 class="mb-1">${highlightText(customer.name, q || name)}</h5>
<p class="mb-1 customer-number">${createCustomerLink(customer.nummer)}</p>
<p class="mb-1">${createAddressLink(customer.strasse, customer.plz, customer.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>` : ''}
</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);
try {
let results;
if (navigator.onLine) {
// Online: Server-Suche
const response = await fetch('/api/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(searchParams)
});
})
.catch(error => {
document.getElementById('loading').style.display = 'none';
});
results = await response.json();
// Speichere die Ergebnisse in IndexedDB
await window.dbHelper.saveCustomers(results);
} else {
// Offline: Lokale Suche
results = await window.dbHelper.searchCustomersOffline(searchParams);
}
lastResults = results;
displayResults(results);
updateResultCounts();
} catch (error) {
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 {
document.getElementById('loading').style.display = 'none';
}
}
// Event-Listener für die Live-Suche

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>