Feat: Offline-Funktionalität mit IndexedDB implementiert

This commit is contained in:
2025-03-18 16:22:57 +01:00
parent 80281d1c2c
commit 57d6daa298
4 changed files with 325 additions and 52 deletions

27
app.py
View File

@@ -330,6 +330,33 @@ def sw():
def offline(): def offline():
return render_template('offline.html') 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

@@ -254,3 +254,81 @@ body {
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;
}

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
};

View File

@@ -14,6 +14,7 @@
<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">
@@ -110,28 +111,49 @@
</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 Registrierung // Service Worker und IndexedDB initialisieren
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', () => { window.addEventListener('load', async () => {
navigator.serviceWorker.register('/sw.js') try {
.then(registration => { const registration = await navigator.serviceWorker.register('/sw.js');
console.log('ServiceWorker registration successful'); console.log('ServiceWorker registration successful');
})
.catch(err => { // IndexedDB initialisieren und erste Synchronisation starten
console.log('ServiceWorker registration failed: ', err); await window.dbHelper.initDB();
}); await window.dbHelper.synchronizeData();
} catch (err) {
console.log('ServiceWorker registration failed: ', err);
}
}); });
} }
// Offline-Status-Überwachung // Offline-Status-Anzeige aktualisieren
window.addEventListener('online', function() { const updateOfflineStatus = () => {
document.body.classList.remove('offline'); const offlineIndicator = document.getElementById('offlineIndicator');
}); const syncStatus = document.getElementById('syncStatus');
window.addEventListener('offline', function() { if (!navigator.onLine) {
document.body.classList.add('offline'); 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 = [];
@@ -309,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();
// Führe die Suche durch // Speichere die Ergebnisse in IndexedDB
fetch('/search?' + params.toString()) await window.dbHelper.saveCustomers(results);
.then(response => response.json()) } else {
.then(data => { // Offline: Lokale Suche
// Verstecke das Lade-Icon results = await window.dbHelper.searchCustomersOffline(searchParams);
document.getElementById('loading').style.display = 'none'; }
if (data.error) { lastResults = results;
return; displayResults(results);
} updateResultCounts();
} catch (error) {
lastResults = data; console.error('Fehler bei der Suche:', error);
updateResultCounts(); document.getElementById('results').innerHTML = '<p>Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</p>';
displayResults(data); } finally {
}) document.getElementById('loading').style.display = 'none';
.catch(error => { }
document.getElementById('loading').style.display = 'none';
});
} }
// Event-Listener für die Live-Suche // Event-Listener für die Live-Suche