feat: add alot
This commit is contained in:
@@ -1,114 +1,68 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../models/payment_method.dart';
|
||||
import '../models/app_error.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/payment_method.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';
|
||||
|
||||
/// Tabs come from a database stream, so every change (local or remote) shows
|
||||
/// up without manual reloads. Actions report to Sentry and rethrow; the view
|
||||
/// decides how to tell the user.
|
||||
class BarScreenViewModel extends ChangeNotifier {
|
||||
final BarTabService barTabService;
|
||||
final InventoryViewModel inventory;
|
||||
final RemoteSyncViewModel remoteSyncViewModel;
|
||||
|
||||
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;
|
||||
BarScreenViewModel({required this.barTabService});
|
||||
|
||||
StreamSubscription<List<BarTab>>? _subscription;
|
||||
List<BarTab> _tabs = [];
|
||||
String? _selectedTabId;
|
||||
bool _isLoading = false;
|
||||
bool _hasLoaded = false;
|
||||
String? _errorMessage;
|
||||
AppError? _loadError;
|
||||
|
||||
List<BarTab> get tabs => _tabs;
|
||||
|
||||
String? get selectedTabId => _selectedTabId;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoading => !_hasLoaded;
|
||||
|
||||
bool get hasLoaded => _hasLoaded;
|
||||
AppError? get loadError => _loadError;
|
||||
|
||||
String? get errorMessage => _errorMessage;
|
||||
BarTab? get selectedTab =>
|
||||
_tabs.firstWhereOrNull((tab) => tab.id == _selectedTabId);
|
||||
|
||||
BarTab? get selectedTab {
|
||||
if (_selectedTabId == null) return null;
|
||||
void ensureLoaded() {
|
||||
if (_subscription != null) return;
|
||||
|
||||
try {
|
||||
return _tabs.firstWhere((tab) => tab.id == _selectedTabId);
|
||||
} catch (e) {
|
||||
debugPrint('BarScreenViewModel: error finding selected tab: $e');
|
||||
return null;
|
||||
}
|
||||
_subscription = barTabService.watchOpenTabs().listen(
|
||||
_onTabs,
|
||||
onError: _onLoadError,
|
||||
);
|
||||
}
|
||||
|
||||
void _onRemoteEvent(RemoteSyncEvent event) {
|
||||
_lastRemoteEvent = event;
|
||||
void _onTabs(List<BarTab> tabs) {
|
||||
_tabs = tabs;
|
||||
_hasLoaded = true;
|
||||
_loadError = null;
|
||||
|
||||
if (selectedTab == null) _selectedTabId = tabs.firstOrNull?.id;
|
||||
|
||||
notifyListeners();
|
||||
if (event.type == RemoteSyncEventType.tabClosedRemotely) {
|
||||
_reloadTabs();
|
||||
}
|
||||
}
|
||||
|
||||
void clearRemoteEvent() {
|
||||
_lastRemoteEvent = null;
|
||||
}
|
||||
void _onLoadError(Object error, StackTrace stack) {
|
||||
debugPrint('BarScreenViewModel: load error: $error');
|
||||
unawaited(Sentry.captureException(error, stackTrace: stack));
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> ensureLoaded() async {
|
||||
if (_hasLoaded || _isLoading) return;
|
||||
|
||||
await load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
if (_isLoading) return;
|
||||
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_hasLoaded = true;
|
||||
_loadError = AppError.loadBar;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_tabs = await barTabService.getOpenTabs();
|
||||
|
||||
if (_selectedTabId == null ||
|
||||
!_tabs.any((tab) => tab.id == _selectedTabId)) {
|
||||
_selectedTabId = _tabs.isEmpty ? null : _tabs.first.id;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('BarScreenViewModel: load error: $e');
|
||||
_errorMessage = 'Could not load bar screen.';
|
||||
} finally {
|
||||
_hasLoaded = true;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadAndSync() async {
|
||||
await load();
|
||||
await remoteSyncViewModel.syncNow();
|
||||
}
|
||||
|
||||
void selectTab(String tabId) {
|
||||
@@ -116,143 +70,72 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> createTab(String customerName) async {
|
||||
try {
|
||||
Future<void> createTab(String customerName) {
|
||||
return _guard('createTab', () async {
|
||||
final tab = await barTabService.createTab(customerName: customerName);
|
||||
_selectedTabId = tab.id;
|
||||
await _reloadTabs();
|
||||
} catch (e, stack) {
|
||||
debugPrint('BarScreenViewModel: createTab error: $e');
|
||||
_errorMessage = 'Could not create tab.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
}
|
||||
selectTab(tab.id);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> renameTab(String tabId, String customerName) async {
|
||||
try {
|
||||
await barTabService.renameTab(tabId: tabId, customerName: customerName);
|
||||
await _reloadTabs();
|
||||
} catch (e, stack) {
|
||||
debugPrint('BarScreenViewModel: renameTab error: $e');
|
||||
_errorMessage = 'Could not rename tab.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
rethrow;
|
||||
}
|
||||
Future<void> renameTab(String tabId, String customerName) {
|
||||
return _guard(
|
||||
'renameTab',
|
||||
() => barTabService.renameTab(tabId: tabId, customerName: customerName),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteTab(String tabId) async {
|
||||
try {
|
||||
final deletedItems = await barTabService.deleteTab(tabId);
|
||||
|
||||
for (final item in deletedItems) {
|
||||
inventory.applyStockDelta(item.productId, item.quantity);
|
||||
}
|
||||
|
||||
await _reloadTabs();
|
||||
} catch (e, stack) {
|
||||
debugPrint('BarScreenViewModel: deleteTab error: $e');
|
||||
_errorMessage = 'Could not delete tab.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
rethrow;
|
||||
}
|
||||
Future<void> deleteTab(String tabId) {
|
||||
return _guard('deleteTab', () => barTabService.deleteTab(tabId));
|
||||
}
|
||||
|
||||
Future<void> addProductToSelectedTab(Product product) async {
|
||||
final tab = selectedTab;
|
||||
|
||||
if (tab == null) return;
|
||||
|
||||
try {
|
||||
await barTabService.addProductToTab(tabId: tab.id, product: product);
|
||||
inventory.applyStockDelta(product.id, -1);
|
||||
|
||||
await _reloadTabs();
|
||||
} catch (e, stack) {
|
||||
if (e is InsufficientStockException) rethrow;
|
||||
|
||||
debugPrint('BarScreenViewModel: addProductToSelectedTab error: $e');
|
||||
_errorMessage = 'Could not add product to tab.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
rethrow;
|
||||
}
|
||||
await _guard(
|
||||
'addProductToSelectedTab',
|
||||
() => barTabService.addProductToTab(tabId: tab.id, product: product),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> changeItemQuantity(TabItem item, int delta) async {
|
||||
if (delta == 0) return;
|
||||
|
||||
try {
|
||||
final actualDelta = await barTabService.adjustTabItemQuantity(
|
||||
tabItemId: item.id,
|
||||
delta: delta,
|
||||
);
|
||||
inventory.applyStockDelta(item.productId, -actualDelta);
|
||||
|
||||
await _reloadTabs();
|
||||
} catch (e, stack) {
|
||||
if (e is InsufficientStockException) rethrow;
|
||||
|
||||
debugPrint('BarScreenViewModel: changeItemQuantity error: $e');
|
||||
_errorMessage = 'Could not update item quantity.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
rethrow;
|
||||
}
|
||||
await _guard(
|
||||
'changeItemQuantity',
|
||||
() =>
|
||||
barTabService.adjustTabItemQuantity(tabItemId: item.id, delta: delta),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> closeSelectedTab({
|
||||
PaymentMethod paymentMethod = PaymentMethod.cash,
|
||||
}) async {
|
||||
final tab = selectedTab;
|
||||
|
||||
if (tab == null) return;
|
||||
|
||||
await _guard(
|
||||
'closeSelectedTab',
|
||||
() => barTabService.closeTab(tab.id, paymentMethod: paymentMethod),
|
||||
);
|
||||
}
|
||||
|
||||
/// Running out of stock is an expected outcome, not a bug worth reporting.
|
||||
Future<T> _guard<T>(String action, Future<T> Function() body) async {
|
||||
try {
|
||||
await barTabService.closeTab(tab.id, paymentMethod: paymentMethod);
|
||||
await _reloadTabs();
|
||||
return await body();
|
||||
} on InsufficientStockException {
|
||||
rethrow;
|
||||
} catch (e, stack) {
|
||||
debugPrint('BarScreenViewModel: closeSelectedTab error: $e');
|
||||
_errorMessage = 'Could not close tab.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
debugPrint('BarScreenViewModel: $action error: $e');
|
||||
unawaited(Sentry.captureException(e, stackTrace: stack));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeTab(
|
||||
String tabId, {
|
||||
PaymentMethod paymentMethod = PaymentMethod.cash,
|
||||
}) async {
|
||||
try {
|
||||
await barTabService.closeTab(tabId, paymentMethod: paymentMethod);
|
||||
await _reloadTabs();
|
||||
} catch (e, stack) {
|
||||
debugPrint('BarScreenViewModel: closeTab error: $e');
|
||||
_errorMessage = 'Could not close tab.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reloadTabs() async {
|
||||
try {
|
||||
_tabs = await barTabService.getOpenTabs();
|
||||
|
||||
if (_selectedTabId == null ||
|
||||
!_tabs.any((tab) => tab.id == _selectedTabId)) {
|
||||
_selectedTabId = _tabs.isEmpty ? null : _tabs.first.id;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e, stack) {
|
||||
debugPrint('BarScreenViewModel: reloadTabs error: $e');
|
||||
_errorMessage = 'Could not reload tabs.';
|
||||
notifyListeners();
|
||||
Sentry.captureException(e, stackTrace: stack);
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user