Compare commits
3 Commits
v1.2.8
...
feature/pw
Author | SHA1 | Date | |
---|---|---|---|
57d6daa298 | |||
80281d1c2c | |||
4c69478fa8 |
39
app.py
39
app.py
@@ -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
|
||||||
@@ -320,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():
|
||||||
|
39
scripts/generate_icons.py
Normal file
39
scripts/generate_icons.py
Normal 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()
|
@@ -253,4 +253,82 @@ body {
|
|||||||
.search-options .form-check-label {
|
.search-options .form-check-label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
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;
|
||||||
}
|
}
|
BIN
static/images/icon-192x192.png
Normal file
BIN
static/images/icon-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/icon-512x512.png
Normal file
BIN
static/images/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
25
static/images/icon.svg
Normal file
25
static/images/icon.svg
Normal 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
144
static/js/db.js
Normal 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
28
static/manifest.json
Normal 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
75
static/sw.js
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
@@ -5,9 +5,16 @@
|
|||||||
<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">
|
||||||
@@ -104,7 +111,50 @@
|
|||||||
</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 = [];
|
||||||
|
|
||||||
@@ -281,46 +331,48 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchCustomers() {
|
async function searchCustomers() {
|
||||||
const q = document.getElementById('q').value;
|
const searchParams = {
|
||||||
const name = document.getElementById('nameInput').value;
|
q: document.getElementById('q').value,
|
||||||
const ort = document.getElementById('ortInput').value;
|
name: document.getElementById('nameInput').value,
|
||||||
const nummer = document.getElementById('nummerInput').value;
|
ort: document.getElementById('ortInput').value,
|
||||||
const plz = document.getElementById('plzInput').value;
|
nummer: document.getElementById('nummerInput').value,
|
||||||
const fachrichtung = document.getElementById('fachrichtungInput').value;
|
plz: document.getElementById('plzInput').value,
|
||||||
const searchOperator = document.querySelector('input[name="searchOperator"]:checked').value;
|
fachrichtung: document.getElementById('fachrichtungInput').value,
|
||||||
|
operator: document.querySelector('input[name="searchOperator"]:checked').value
|
||||||
|
};
|
||||||
|
|
||||||
// Zeige das Lade-Icon
|
|
||||||
document.getElementById('loading').style.display = 'block';
|
document.getElementById('loading').style.display = 'block';
|
||||||
|
|
||||||
// Baue die Suchanfrage
|
try {
|
||||||
const params = new URLSearchParams();
|
let results;
|
||||||
if (q) params.append('q', q);
|
if (navigator.onLine) {
|
||||||
if (name) params.append('name', name);
|
// Online: Server-Suche
|
||||||
if (ort) params.append('ort', ort);
|
const response = await fetch('/api/search', {
|
||||||
if (nummer) params.append('nummer', nummer);
|
method: 'POST',
|
||||||
if (plz) params.append('plz', plz);
|
headers: {
|
||||||
if (fachrichtung) params.append('fachrichtung', fachrichtung);
|
'Content-Type': 'application/json',
|
||||||
if (searchOperator) params.append('operator', searchOperator);
|
},
|
||||||
|
body: JSON.stringify(searchParams)
|
||||||
|
});
|
||||||
|
results = await response.json();
|
||||||
|
|
||||||
|
// Speichere die Ergebnisse in IndexedDB
|
||||||
|
await window.dbHelper.saveCustomers(results);
|
||||||
|
} else {
|
||||||
|
// Offline: Lokale Suche
|
||||||
|
results = await window.dbHelper.searchCustomersOffline(searchParams);
|
||||||
|
}
|
||||||
|
|
||||||
// Führe die Suche durch
|
lastResults = results;
|
||||||
fetch('/search?' + params.toString())
|
displayResults(results);
|
||||||
.then(response => response.json())
|
updateResultCounts();
|
||||||
.then(data => {
|
} catch (error) {
|
||||||
// Verstecke das Lade-Icon
|
console.error('Fehler bei der Suche:', error);
|
||||||
document.getElementById('loading').style.display = 'none';
|
document.getElementById('results').innerHTML = '<p>Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</p>';
|
||||||
|
} finally {
|
||||||
if (data.error) {
|
document.getElementById('loading').style.display = 'none';
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
lastResults = data;
|
|
||||||
updateResultCounts();
|
|
||||||
displayResults(data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
document.getElementById('loading').style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event-Listener für die Live-Suche
|
// Event-Listener für die Live-Suche
|
||||||
|
58
templates/offline.html
Normal file
58
templates/offline.html
Normal 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>
|
Reference in New Issue
Block a user