import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:sentry_flutter/sentry_flutter.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 '../services/bar_tab_service.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; BarScreenViewModel({required this.barTabService}); StreamSubscription>? _subscription; List _tabs = []; String? _selectedTabId; bool _hasLoaded = false; AppError? _loadError; List get tabs => _tabs; String? get selectedTabId => _selectedTabId; bool get isLoading => !_hasLoaded; AppError? get loadError => _loadError; BarTab? get selectedTab => _tabs.firstWhereOrNull((tab) => tab.id == _selectedTabId); bool hasOpenTabNamed(String name, {String? exceptTabId}) { final normalized = name.trim().toLowerCase(); return _tabs.any( (tab) => tab.id != exceptTabId && tab.customerName.trim().toLowerCase() == normalized, ); } void ensureLoaded() { if (_subscription != null) return; _subscription = barTabService.watchOpenTabs().listen( _onTabs, onError: _onLoadError, ); } void _onTabs(List tabs) { _tabs = tabs; _hasLoaded = true; _loadError = null; if (selectedTab == null) _selectedTabId = tabs.firstOrNull?.id; notifyListeners(); } void _onLoadError(Object error, StackTrace stack) { debugPrint('BarScreenViewModel: load error: $error'); unawaited(Sentry.captureException(error, stackTrace: stack)); _hasLoaded = true; _loadError = AppError.loadBar; notifyListeners(); } void selectTab(String tabId) { _selectedTabId = tabId; notifyListeners(); } Future createTab(String customerName) { return _guard('createTab', () async { final tab = await barTabService.createTab(customerName: customerName); selectTab(tab.id); }); } Future renameTab(String tabId, String customerName) { return _guard( 'renameTab', () => barTabService.renameTab(tabId: tabId, customerName: customerName), ); } Future deleteTab(String tabId) { return _guard('deleteTab', () => barTabService.deleteTab(tabId)); } Future addProductToSelectedTab(Product product) async { final tab = selectedTab; if (tab == null) return; await _guard( 'addProductToSelectedTab', () => barTabService.addProductToTab(tabId: tab.id, product: product), ); } Future changeItemQuantity(TabItem item, int delta) async { if (delta == 0) return; await _guard( 'changeItemQuantity', () => barTabService.adjustTabItemQuantity(tabItemId: item.id, delta: delta), ); } Future closeSelectedTab({ PaymentMethod paymentMethod = PaymentMethod.cash, }) async { final tab = selectedTab; if (tab == null) return; await _guard( 'closeSelectedTab', () => barTabService.closeTab(tab.id, paymentMethod: paymentMethod), ); } Future _guard(String action, Future Function() body) async { try { return await body(); } catch (e, stack) { debugPrint('BarScreenViewModel: $action error: $e'); unawaited(Sentry.captureException(e, stackTrace: stack)); rethrow; } } @override void dispose() { _subscription?.cancel(); super.dispose(); } }