142 lines
3.6 KiB
Dart
142 lines
3.6 KiB
Dart
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';
|
|
import '../services/product_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<List<BarTab>>? _subscription;
|
|
List<BarTab> _tabs = [];
|
|
String? _selectedTabId;
|
|
bool _hasLoaded = false;
|
|
AppError? _loadError;
|
|
|
|
List<BarTab> get tabs => _tabs;
|
|
|
|
String? get selectedTabId => _selectedTabId;
|
|
|
|
bool get isLoading => !_hasLoaded;
|
|
|
|
AppError? get loadError => _loadError;
|
|
|
|
BarTab? get selectedTab =>
|
|
_tabs.firstWhereOrNull((tab) => tab.id == _selectedTabId);
|
|
|
|
void ensureLoaded() {
|
|
if (_subscription != null) return;
|
|
|
|
_subscription = barTabService.watchOpenTabs().listen(
|
|
_onTabs,
|
|
onError: _onLoadError,
|
|
);
|
|
}
|
|
|
|
void _onTabs(List<BarTab> 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<void> createTab(String customerName) {
|
|
return _guard('createTab', () async {
|
|
final tab = await barTabService.createTab(customerName: customerName);
|
|
selectTab(tab.id);
|
|
});
|
|
}
|
|
|
|
Future<void> renameTab(String tabId, String customerName) {
|
|
return _guard(
|
|
'renameTab',
|
|
() => barTabService.renameTab(tabId: tabId, customerName: customerName),
|
|
);
|
|
}
|
|
|
|
Future<void> deleteTab(String tabId) {
|
|
return _guard('deleteTab', () => barTabService.deleteTab(tabId));
|
|
}
|
|
|
|
Future<void> addProductToSelectedTab(Product product) async {
|
|
final tab = selectedTab;
|
|
if (tab == null) return;
|
|
|
|
await _guard(
|
|
'addProductToSelectedTab',
|
|
() => barTabService.addProductToTab(tabId: tab.id, product: product),
|
|
);
|
|
}
|
|
|
|
Future<void> changeItemQuantity(TabItem item, int delta) async {
|
|
if (delta == 0) return;
|
|
|
|
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 {
|
|
return await body();
|
|
} on InsufficientStockException {
|
|
rethrow;
|
|
} catch (e, stack) {
|
|
debugPrint('BarScreenViewModel: $action error: $e');
|
|
unawaited(Sentry.captureException(e, stackTrace: stack));
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_subscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|