feat: add remote pull and push
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
@@ -5,15 +7,29 @@ import '../models/payment_method.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/tab_item.dart';
|
||||
import '../models/remote_command.dart';
|
||||
import '../services/bar_tab_service.dart';
|
||||
import '../services/product_service.dart';
|
||||
import 'inventory_view_model.dart';
|
||||
import 'remote_sync_view_model.dart';
|
||||
|
||||
class BarScreenViewModel extends ChangeNotifier {
|
||||
final BarTabService barTabService;
|
||||
final InventoryViewModel inventory;
|
||||
final RemoteSyncViewModel remoteSyncViewModel;
|
||||
|
||||
BarScreenViewModel({required this.barTabService, required this.inventory});
|
||||
BarScreenViewModel({
|
||||
required this.barTabService,
|
||||
required this.inventory,
|
||||
required this.remoteSyncViewModel,
|
||||
}) {
|
||||
_eventSubscription = remoteSyncViewModel.syncService.events.listen(_onRemoteEvent);
|
||||
}
|
||||
|
||||
StreamSubscription<RemoteSyncEvent>? _eventSubscription;
|
||||
|
||||
RemoteSyncEvent? _lastRemoteEvent;
|
||||
RemoteSyncEvent? get lastRemoteEvent => _lastRemoteEvent;
|
||||
|
||||
List<BarTab> _tabs = [];
|
||||
String? _selectedTabId;
|
||||
@@ -42,6 +58,24 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _onRemoteEvent(RemoteSyncEvent event) {
|
||||
_lastRemoteEvent = event;
|
||||
notifyListeners();
|
||||
if (event.type == RemoteSyncEventType.tabClosedRemotely) {
|
||||
_reloadTabs();
|
||||
}
|
||||
}
|
||||
|
||||
void clearRemoteEvent() {
|
||||
_lastRemoteEvent = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> ensureLoaded() async {
|
||||
if (_hasLoaded || _isLoading) return;
|
||||
|
||||
@@ -72,6 +106,11 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadAndSync() async {
|
||||
await load();
|
||||
await remoteSyncViewModel.syncNow();
|
||||
}
|
||||
|
||||
void selectTab(String tabId) {
|
||||
_selectedTabId = tabId;
|
||||
notifyListeners();
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../models/venue.dart';
|
||||
import '../services/remote_sync_service.dart';
|
||||
import '../utils/app_config.dart';
|
||||
|
||||
enum RegistrationStep { initial, loadingVenues, venuesLoaded, error, registering, registered }
|
||||
|
||||
class RegistrationViewModel extends ChangeNotifier {
|
||||
final RemoteSyncService syncService;
|
||||
|
||||
RegistrationViewModel({required this.syncService});
|
||||
|
||||
RegistrationStep _step = RegistrationStep.initial;
|
||||
RegistrationStep get step => _step;
|
||||
|
||||
List<Venue> _venues = [];
|
||||
List<Venue> get venues => _venues;
|
||||
|
||||
Venue? _selectedVenue;
|
||||
Venue? get selectedVenue => _selectedVenue;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
void selectVenue(Venue venue) {
|
||||
_selectedVenue = venue;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> fetchVenues() async {
|
||||
_step = RegistrationStep.loadingVenues;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http
|
||||
.get(Uri.parse('$kRemoteServerUrl/venues/'))
|
||||
.timeout(const Duration(seconds: 15));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body) as List<dynamic>;
|
||||
_venues = data
|
||||
.map((e) => Venue.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
_step = RegistrationStep.venuesLoaded;
|
||||
} else {
|
||||
_errorMessage = 'Failed to load venues (HTTP ${response.statusCode})';
|
||||
_step = RegistrationStep.error;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('RegistrationViewModel: fetchVenues error: $e');
|
||||
_errorMessage = e.toString();
|
||||
_step = RegistrationStep.error;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> registerDevice({
|
||||
required String deviceName,
|
||||
required String registrationCode,
|
||||
}) async {
|
||||
final venue = _selectedVenue;
|
||||
if (venue == null) return false;
|
||||
|
||||
_step = RegistrationStep.registering;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$kRemoteServerUrl/devices/register'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'venueId': venue.id,
|
||||
'name': deviceName,
|
||||
'registrationCode': registrationCode,
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final deviceId = data['deviceId'] as String;
|
||||
final token = data['token'] as String;
|
||||
|
||||
await syncService.setCredentials(
|
||||
deviceId: deviceId,
|
||||
token: token,
|
||||
venueId: venue.id,
|
||||
serverUrl: kRemoteServerUrl,
|
||||
);
|
||||
|
||||
_step = RegistrationStep.registered;
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
String message;
|
||||
try {
|
||||
final errorData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
message = errorData['message'] as String? ?? 'Registration failed';
|
||||
} catch (_) {
|
||||
message = 'Registration failed (HTTP ${response.statusCode})';
|
||||
}
|
||||
_errorMessage = message;
|
||||
_step = RegistrationStep.venuesLoaded;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('RegistrationViewModel: registerDevice error: $e');
|
||||
_errorMessage = e.toString();
|
||||
_step = RegistrationStep.venuesLoaded;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> retry() async {
|
||||
await fetchVenues();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_step = RegistrationStep.initial;
|
||||
_venues = [];
|
||||
_selectedVenue = null;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../database/app_database.dart';
|
||||
import '../models/remote_command.dart';
|
||||
import '../services/remote_sync_service.dart';
|
||||
import '../utils/app_config.dart';
|
||||
|
||||
class RemoteSyncViewModel extends ChangeNotifier {
|
||||
final RemoteSyncService syncService;
|
||||
|
||||
RemoteSyncViewModel({required this.syncService}) {
|
||||
_eventSubscription = syncService.events.listen(_onEvent);
|
||||
}
|
||||
|
||||
StreamSubscription<RemoteSyncEvent>? _eventSubscription;
|
||||
|
||||
List<RemoteSyncEvent> _recentEvents = [];
|
||||
|
||||
List<RemoteSyncEvent> get recentEvents => _recentEvents;
|
||||
|
||||
bool _isSyncing = false;
|
||||
bool get isSyncing => _isSyncing;
|
||||
|
||||
bool _isConfigured = false;
|
||||
bool get isConfigured => _isConfigured;
|
||||
|
||||
DateTime? _lastSyncAt;
|
||||
DateTime? get lastSyncAt => _lastSyncAt;
|
||||
|
||||
String? _lastError;
|
||||
String? get lastError => _lastError;
|
||||
|
||||
String? _venueId;
|
||||
String? get venueId => _venueId;
|
||||
|
||||
List<ProcessedRemoteCommandRow> _activity = [];
|
||||
List<ProcessedRemoteCommandRow> get activity => _activity;
|
||||
|
||||
bool _isRegistering = false;
|
||||
bool get isRegistering => _isRegistering;
|
||||
|
||||
String? _registrationError;
|
||||
String? get registrationError => _registrationError;
|
||||
|
||||
Future<void> init() async {
|
||||
_isConfigured = await syncService.isConfigured;
|
||||
_venueId = await syncService.venueId;
|
||||
_lastSyncAt = await syncService.lastSuccessfulSyncAt;
|
||||
await loadActivity();
|
||||
if (_isConfigured) {
|
||||
syncService.startPeriodicSync();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> refreshStatus() async {
|
||||
_isConfigured = await syncService.isConfigured;
|
||||
_venueId = await syncService.venueId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> syncNow() async {
|
||||
_isSyncing = true;
|
||||
_lastError = null;
|
||||
notifyListeners();
|
||||
|
||||
await syncService.sync();
|
||||
|
||||
_isSyncing = syncService.isSyncing;
|
||||
_lastSyncAt = await syncService.lastSuccessfulSyncAt;
|
||||
await loadActivity();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadActivity() async {
|
||||
try {
|
||||
_activity = await syncService.getRecentActivity();
|
||||
} catch (e) {
|
||||
debugPrint('RemoteSyncViewModel: loadActivity error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> registerDevice({
|
||||
required String venueId,
|
||||
required String deviceName,
|
||||
required String registrationCode,
|
||||
}) async {
|
||||
_isRegistering = true;
|
||||
_registrationError = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$kRemoteServerUrl/devices/register'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'venueId': venueId,
|
||||
'name': deviceName,
|
||||
'registrationCode': registrationCode,
|
||||
}),
|
||||
).timeout(const Duration(seconds: 15));
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final deviceId = data['deviceId'] as String;
|
||||
final token = data['token'] as String;
|
||||
|
||||
await syncService.setCredentials(
|
||||
deviceId: deviceId,
|
||||
token: token,
|
||||
venueId: venueId,
|
||||
serverUrl: kRemoteServerUrl,
|
||||
);
|
||||
|
||||
_isConfigured = true;
|
||||
_venueId = venueId;
|
||||
_isRegistering = false;
|
||||
notifyListeners();
|
||||
|
||||
syncService.startPeriodicSync();
|
||||
await syncNow();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
final body = response.body;
|
||||
String message;
|
||||
try {
|
||||
final errorData = jsonDecode(body) as Map<String, dynamic>;
|
||||
message = errorData['message'] as String? ?? 'Registration failed';
|
||||
} catch (_) {
|
||||
message = 'Registration failed (HTTP ${response.statusCode})';
|
||||
}
|
||||
_registrationError = message;
|
||||
_isRegistering = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('RemoteSyncViewModel: registration error: $e');
|
||||
_registrationError = e.toString();
|
||||
_isRegistering = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unlink() async {
|
||||
syncService.stopPeriodicSync();
|
||||
await syncService.clearCredentials();
|
||||
_isConfigured = false;
|
||||
_venueId = null;
|
||||
_lastSyncAt = null;
|
||||
_lastError = null;
|
||||
_recentEvents = [];
|
||||
_activity = [];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void startSync() {
|
||||
syncService.startPeriodicSync();
|
||||
syncNow();
|
||||
}
|
||||
|
||||
void _onEvent(RemoteSyncEvent event) {
|
||||
_recentEvents = [event, ..._recentEvents.take(49)];
|
||||
_lastSyncAt = DateTime.now();
|
||||
|
||||
if (event.type == RemoteSyncEventType.syncFailed) {
|
||||
_lastError = event.message;
|
||||
} else if (event.type == RemoteSyncEventType.syncCompleted) {
|
||||
_lastError = null;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void pause() {
|
||||
syncService.pause();
|
||||
}
|
||||
|
||||
void resume() {
|
||||
syncService.resume();
|
||||
syncNow();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventSubscription?.cancel();
|
||||
syncService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user