Feat: Offline-Funktionalität mit IndexedDB implementiert
This commit is contained in:
27
app.py
27
app.py
@@ -330,6 +330,33 @@ def sw():
|
||||
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():
|
||||
|
@@ -253,4 +253,82 @@ body {
|
||||
.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;
|
||||
}
|
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
|
||||
};
|
@@ -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://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">
|
||||
@@ -110,28 +111,49 @@
|
||||
</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 Registrierung
|
||||
// Service Worker und IndexedDB initialisieren
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('ServiceWorker registration successful');
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('ServiceWorker registration failed: ', err);
|
||||
});
|
||||
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-Überwachung
|
||||
window.addEventListener('online', function() {
|
||||
document.body.classList.remove('offline');
|
||||
});
|
||||
// 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('offline', function() {
|
||||
document.body.classList.add('offline');
|
||||
});
|
||||
window.addEventListener('online', updateOfflineStatus);
|
||||
window.addEventListener('offline', updateOfflineStatus);
|
||||
updateOfflineStatus();
|
||||
|
||||
let searchTimeout;
|
||||
let lastResults = [];
|
||||
@@ -309,46 +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;
|
||||
const searchOperator = document.querySelector('input[name="searchOperator"]:checked').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);
|
||||
if (searchOperator) params.append('operator', searchOperator);
|
||||
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)
|
||||
});
|
||||
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
|
||||
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();
|
||||
displayResults(data);
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
});
|
||||
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
|
||||
|
Reference in New Issue
Block a user