chore: initial commit Dutra Store POS app
This commit is contained in:
156
lib/services/offline_queue_service.dart
Normal file
156
lib/services/offline_queue_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
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<Map<String, dynamic>> _queue = [];
|
||||
bool _isOnline = true;
|
||||
bool _syncing = false;
|
||||
|
||||
static const Duration _timeout = Duration(seconds: 30);
|
||||
|
||||
Future<T> _withTimeout<T>(Future<T> future, [String? operation]) async {
|
||||
try {
|
||||
return await future.timeout(
|
||||
_timeout,
|
||||
onTimeout: () => throw TimeoutException('${operation ?? 'Operasi'} timeout setelah $_timeout'),
|
||||
);
|
||||
} on TimeoutException catch (e) {
|
||||
print('OfflineQueue timeout: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> get queue => List.unmodifiable(_queue);
|
||||
bool get isOnline => _isOnline;
|
||||
bool get hasPendingItems => _queue.isNotEmpty;
|
||||
|
||||
Future<void> init() async {
|
||||
await _loadQueue();
|
||||
_connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
|
||||
await _checkConnection();
|
||||
if (_isOnline) await _processQueue();
|
||||
}
|
||||
|
||||
Future<void> _loadQueue() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final queueJson = prefs.getString('offline_queue');
|
||||
if (queueJson != null) {
|
||||
final List<dynamic> decoded = json.decode(queueJson);
|
||||
_queue = decoded.cast<Map<String, dynamic>>();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveQueue() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('offline_queue', json.encode(_queue));
|
||||
}
|
||||
|
||||
Future<void> _checkConnection() async {
|
||||
final result = await _connectivity.checkConnectivity();
|
||||
_updateConnectionStatus(result);
|
||||
}
|
||||
|
||||
void _updateConnectionStatus(List<ConnectivityResult> result) {
|
||||
_isOnline = result.any((r) => r != ConnectivityResult.none);
|
||||
if (_isOnline && _queue.isNotEmpty) {
|
||||
_processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addToQueue(Map<String, dynamic> transaction) async {
|
||||
_queue.add(transaction);
|
||||
await _saveQueue();
|
||||
if (_isOnline) {
|
||||
await _processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<void> _processTransaction(Map<String, dynamic> transaction) async {
|
||||
final sale = transaction['sale'] as Map<String, dynamic>;
|
||||
final items = (transaction['items'] as List?) ?? [];
|
||||
|
||||
final saleId = sale['id'] as String;
|
||||
final existing = await _withTimeout(
|
||||
_supabase
|
||||
.from('sales')
|
||||
.select('id, total_cost')
|
||||
.eq('id', saleId)
|
||||
.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),
|
||||
'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)
|
||||
.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),
|
||||
'Update product stock',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearQueue() async {
|
||||
_queue.clear();
|
||||
await _saveQueue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user