import 'package:path/path.dart' as p; import 'package:sqflite/sqflite.dart'; import '../api/api_client.dart'; /// Gleiche Idee wie in der React-PWA (frontend/src/offline/queue.ts): /// Eingaben werden lokal zwischengespeichert, sobald eingegeben, nicht erst /// bei Übertragung verworfen, wenn die Verbindung kurz fehlt. sqflite statt /// IndexedDB, weil das die etablierte lokale-DB-Lösung für Flutter ist - /// gleiche Rolle, andere Plattform (Prompt 17-Härtung für den Android-Client). class QueueItem { QueueItem({ required this.kontrolleId, required this.materialId, required this.istmenge, this.ablaufdatum, this.chargennummer, this.seriennummer, this.status = 'wartet', this.fehlerText, }); final String kontrolleId; final int materialId; final String istmenge; final String? ablaufdatum; final String? chargennummer; final String? seriennummer; String status; // wartet | wird_uebertragen | fehler String? fehlerText; Map toRow() => { 'kontrolle_id': kontrolleId, 'material_id': materialId, 'istmenge': istmenge, 'ablaufdatum': ablaufdatum, 'chargennummer': chargennummer, 'seriennummer': seriennummer, 'status': status, 'fehler_text': fehlerText, }; factory QueueItem.fromRow(Map row) => QueueItem( kontrolleId: row['kontrolle_id'] as String, materialId: row['material_id'] as int, istmenge: row['istmenge'] as String, ablaufdatum: row['ablaufdatum'] as String?, chargennummer: row['chargennummer'] as String?, seriennummer: row['seriennummer'] as String?, status: row['status'] as String, fehlerText: row['fehler_text'] as String?, ); } class OfflineQueue { OfflineQueue._(); static final OfflineQueue instance = OfflineQueue._(); Database? _db; bool _synchronisierungLaeuft = false; Future _database() async { final bestehend = _db; if (bestehend != null) return bestehend; final pfad = p.join(await getDatabasesPath(), 'mabea_offline_queue.db'); final db = await openDatabase( pfad, version: 1, onCreate: (db, version) => db.execute(''' CREATE TABLE queue ( kontrolle_id TEXT NOT NULL, material_id INTEGER NOT NULL, istmenge TEXT NOT NULL, ablaufdatum TEXT, chargennummer TEXT, seriennummer TEXT, status TEXT NOT NULL, fehler_text TEXT, PRIMARY KEY (kontrolle_id, material_id) ) '''), ); _db = db; return db; } Future enqueue({ required String kontrolleId, required int materialId, required String istmenge, String? ablaufdatum, String? chargennummer, String? seriennummer, }) async { final db = await _database(); await db.insert( 'queue', QueueItem( kontrolleId: kontrolleId, materialId: materialId, istmenge: istmenge, ablaufdatum: ablaufdatum, chargennummer: chargennummer, seriennummer: seriennummer, ).toRow(), conflictAlgorithm: ConflictAlgorithm.replace, ); } Future> eintraegeFuerKontrolle(String kontrolleId) async { final db = await _database(); final rows = await db.query('queue', where: 'kontrolle_id = ?', whereArgs: [kontrolleId]); return rows.map(QueueItem.fromRow).toList(); } Future entferneFehlerEintrag(String kontrolleId, int materialId) async { final db = await _database(); await db.delete( 'queue', where: 'kontrolle_id = ? AND material_id = ?', whereArgs: [kontrolleId, materialId], ); } Future _setzeStatus( String kontrolleId, int materialId, String status, { String? fehlerText, }) async { final db = await _database(); await db.update( 'queue', {'status': status, 'fehler_text': fehlerText}, where: 'kontrolle_id = ? AND material_id = ?', whereArgs: [kontrolleId, materialId], ); } Future _sendeEintrag(ApiClient apiClient, QueueItem item) async { await _setzeStatus(item.kontrolleId, item.materialId, 'wird_uebertragen'); try { await apiClient.request( '/kontrollen/${item.kontrolleId}/positionen/${item.materialId}', method: 'PUT', body: { 'istmenge': item.istmenge, 'ablaufdatum': item.ablaufdatum, 'chargennummer': item.chargennummer, 'seriennummer': item.seriennummer, }, ); // Idempotent (Backend-Upsert, siehe frontend/src/offline/queue.ts) - aus // Queue entfernen ist sicher, auch bei mehrfachem Sendeversuch vor Erfolg. final db = await _database(); await db.delete( 'queue', where: 'kontrolle_id = ? AND material_id = ?', whereArgs: [item.kontrolleId, item.materialId], ); } on ApiException catch (error) { if (error.status >= 400 && error.status < 500) { await _setzeStatus( item.kontrolleId, item.materialId, 'fehler', fehlerText: 'Server hat abgelehnt (${error.status}) - bitte Kontrolle neu prüfen.', ); } else { await _setzeStatus(item.kontrolleId, item.materialId, 'wartet'); } rethrow; } catch (error) { // Netzwerkfehler (kein ApiException) - bleibt "wartet", nächster Sync // versucht es erneut. await _setzeStatus(item.kontrolleId, item.materialId, 'wartet'); rethrow; } } /// Wird bei Reconnect und periodisch aufgerufen. Bricht bei Netzwerkfehlern /// früh ab (weitere Versuche würden ohnehin scheitern, solange offline). Future synchronisiere(ApiClient apiClient) async { if (_synchronisierungLaeuft) return; _synchronisierungLaeuft = true; try { final db = await _database(); final rows = await db.query('queue'); for (final row in rows) { final item = QueueItem.fromRow(row); if (item.status == 'fehler') continue; // braucht bewusste Nutzer-Aktion try { await _sendeEintrag(apiClient, item); } on ApiException { // Client-/Server-Fehler: schon in _sendeEintrag behandelt, weiter mit // dem nächsten Eintrag. } catch (_) { return; // vermutlich offline - restliche Queue in Ruhe lassen } } } finally { _synchronisierungLaeuft = false; } } }