import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class OfflineQueueService { static final OfflineQueueService _instance = OfflineQueueService._internal(); factory OfflineQueueService() => _instance; OfflineQueueService._internal(); final SupabaseClient _supabase = Supabase.instance.client; final Connectivity _connectivity = Connectivity(); List> _queue = []; bool _isOnline = true; bool _syncing = false; static const Duration _timeout = Duration(seconds: 30); Future _withTimeout(Future future, [String? operation]) async { try { return await future.timeout( _timeout, onTimeout: () => throw TimeoutException('${operation ?? 'Operasi'} timeout setelah $_timeout'), ); } on TimeoutException catch (e) { debugPrint('OfflineQueue timeout: $e'); rethrow; } } List> get queue => List.unmodifiable(_queue); bool get isOnline => _isOnline; bool get hasPendingItems => _queue.isNotEmpty; Future init() async { await _loadQueue(); _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); await _checkConnection(); if (_isOnline) await _processQueue(); } Future _loadQueue() async { final prefs = await SharedPreferences.getInstance(); final queueJson = prefs.getString('offline_queue'); if (queueJson != null) { final List decoded = json.decode(queueJson); _queue = decoded.cast>(); } } Future _saveQueue() async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('offline_queue', json.encode(_queue)); } Future _checkConnection() async { final result = await _connectivity.checkConnectivity(); _updateConnectionStatus(result); } void _updateConnectionStatus(List result) { _isOnline = result.any((r) => r != ConnectivityResult.none); if (_isOnline && _queue.isNotEmpty) { _processQueue(); } } Future addToQueue(Map transaction) async { _queue.add(transaction); await _saveQueue(); if (_isOnline) { await _processQueue(); } } Future _processQueue() async { if (_syncing || !_isOnline || _queue.isEmpty) return; _syncing = true; while (_queue.isNotEmpty && _isOnline) { final item = _queue.first; try { await _processTransaction(item); _queue.removeAt(0); await _saveQueue(); } catch (e) { _syncing = false; return; } } _syncing = false; } Future _processTransaction(Map transaction) async { final sale = transaction['sale'] as Map; final items = (transaction['items'] as List?) ?? []; final saleId = sale['id'] as String; final storeId = sale['store_id'] as String?; final existing = await _withTimeout( _supabase .from('sales') .select('id, total_cost') .eq('id', saleId) .eq('store_id', storeId ?? '00000000-0000-0000-0000-000000000000') .maybeSingle(), 'Check existing sale', ); if (existing == null) { await _withTimeout( _supabase.from('sales').insert(sale), 'Insert queued sale', ); } else { final existingTotalCost = (existing['total_cost'] as num?)?.toDouble() ?? 0; if (existingTotalCost <= 0 && sale['total_cost'] != null) { await _withTimeout( _supabase .from('sales') .update({'total_cost': sale['total_cost']}) .eq('id', saleId) .eq('store_id', storeId ?? '00000000-0000-0000-0000-000000000000'), 'Update sale total_cost', ); } } for (final it in items) { final productId = it['product_id'] as String; final qty = (it['qty'] as num).toInt(); final product = await _withTimeout( _supabase .from('products') .select() .eq('id', productId) .eq('store_id', storeId ?? '00000000-0000-0000-0000-000000000000') .maybeSingle(), 'Load product for stock update', ); if (product != null) { final currentStock = (product['stock'] as num).toInt(); final newStock = (currentStock - qty).clamp(0, 1 << 62); await _withTimeout( _supabase .from('products') .update({'stock': newStock}) .eq('id', productId) .eq('store_id', storeId ?? '00000000-0000-0000-0000-000000000000'), 'Update product stock', ); } } } Future clearQueue() async { _queue.clear(); await _saveQueue(); } }