feat: New Open Tab dialog with search and optional customer email. Drinks are now grouped per order. Payments trigger cha-ching and money rain.
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../models/app_error.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
@@ -15,9 +16,20 @@ import '../services/bar_tab_service.dart';
|
||||
/// 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;
|
||||
/// Drinks rung up on the same tab within this time of the previous one are
|
||||
/// one order; a later drink starts a new order.
|
||||
static const orderTimeout = Duration(minutes: 5);
|
||||
|
||||
BarScreenViewModel({required this.barTabService});
|
||||
final BarTabService barTabService;
|
||||
final DateTime Function() _now;
|
||||
final _uuid = const Uuid();
|
||||
|
||||
BarScreenViewModel({required this.barTabService, DateTime Function()? now})
|
||||
: _now = now ?? DateTime.now;
|
||||
|
||||
/// The order being rung up: selecting a tab (again) or a pause of
|
||||
/// [orderTimeout] starts a new one.
|
||||
({String tabId, String orderId, DateTime lastAddedAt})? _order;
|
||||
|
||||
StreamSubscription<List<BarTab>>? _subscription;
|
||||
List<BarTab> _tabs = [];
|
||||
@@ -25,7 +37,30 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
bool _hasLoaded = false;
|
||||
AppError? _loadError;
|
||||
|
||||
List<BarTab> get tabs => _tabs;
|
||||
/// Tabs opened on this tablet that have no drinks yet. Only kept in memory:
|
||||
/// after a restart the list starts empty again.
|
||||
final Set<String> _openedTabIds = {};
|
||||
|
||||
/// Every tab the venue knows, open or not.
|
||||
List<BarTab> get allTabs => _tabs;
|
||||
|
||||
/// The tabs on the bar screen: opened here, with drinks on them, or
|
||||
/// selected.
|
||||
List<BarTab> get tabs => [
|
||||
for (final tab in _tabs)
|
||||
if (_isOpen(tab)) tab,
|
||||
];
|
||||
|
||||
/// The tabs that can be opened from the Open Tab dialog.
|
||||
List<BarTab> get closedTabs => [
|
||||
for (final tab in _tabs)
|
||||
if (!_isOpen(tab)) tab,
|
||||
];
|
||||
|
||||
bool _isOpen(BarTab tab) =>
|
||||
tab.itemCount > 0 ||
|
||||
_openedTabIds.contains(tab.id) ||
|
||||
tab.id == _selectedTabId;
|
||||
|
||||
String? get selectedTabId => _selectedTabId;
|
||||
|
||||
@@ -55,11 +90,22 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _onTabs(List<BarTab> tabs) {
|
||||
// A tab whose drinks are all gone (settled elsewhere, or taken off) is
|
||||
// closed again; while it is selected it stays on screen
|
||||
final previousCounts = {for (final tab in _tabs) tab.id: tab.itemCount};
|
||||
for (final tab in tabs) {
|
||||
if ((previousCounts[tab.id] ?? 0) > 0 && tab.itemCount == 0) {
|
||||
_openedTabIds.remove(tab.id);
|
||||
}
|
||||
}
|
||||
|
||||
_tabs = tabs;
|
||||
_hasLoaded = true;
|
||||
_loadError = null;
|
||||
|
||||
if (selectedTab == null) _selectedTabId = tabs.firstOrNull?.id;
|
||||
final ids = {for (final tab in tabs) tab.id};
|
||||
_openedTabIds.retainAll(ids);
|
||||
if (!ids.contains(_selectedTabId)) _selectedTabId = null;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -75,13 +121,44 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
|
||||
void selectTab(String tabId) {
|
||||
_selectedTabId = tabId;
|
||||
_order = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> createTab(String customerName) {
|
||||
/// Leaves no tab selected, so products can't be added by accident.
|
||||
void deselectTab() {
|
||||
_selectedTabId = null;
|
||||
_order = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// The order a drink rung up on [tabId] now is part of.
|
||||
String _orderFor(String tabId) {
|
||||
final now = _now();
|
||||
final order = _order;
|
||||
final orderId =
|
||||
order != null &&
|
||||
order.tabId == tabId &&
|
||||
now.difference(order.lastAddedAt) < orderTimeout
|
||||
? order.orderId
|
||||
: _uuid.v4();
|
||||
_order = (tabId: tabId, orderId: orderId, lastAddedAt: now);
|
||||
return orderId;
|
||||
}
|
||||
|
||||
/// Puts [tabId] on the bar screen and selects it.
|
||||
void openTab(String tabId) {
|
||||
_openedTabIds.add(tabId);
|
||||
selectTab(tabId);
|
||||
}
|
||||
|
||||
Future<void> createTab(String customerName, {String? email}) {
|
||||
return _guard('createTab', () async {
|
||||
final tab = await barTabService.createTab(customerName: customerName);
|
||||
selectTab(tab.id);
|
||||
final tab = await barTabService.createTab(
|
||||
customerName: customerName,
|
||||
email: email,
|
||||
);
|
||||
openTab(tab.id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,17 +179,30 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
|
||||
await _guard(
|
||||
'addProductToSelectedTab',
|
||||
() => barTabService.addProductToTab(tabId: tab.id, product: product),
|
||||
() => barTabService.addProductToTab(
|
||||
tabId: tab.id,
|
||||
product: product,
|
||||
orderId: _orderFor(tab.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> changeItemQuantity(TabItem item, int delta) async {
|
||||
/// More drinks go into the current order; taking drinks off takes them off
|
||||
/// order [orderId], the one they are shown in.
|
||||
Future<void> changeItemQuantity(
|
||||
TabItem item,
|
||||
int delta,
|
||||
String? orderId,
|
||||
) async {
|
||||
if (delta == 0) return;
|
||||
|
||||
await _guard(
|
||||
'changeItemQuantity',
|
||||
() =>
|
||||
barTabService.adjustTabItemQuantity(tabItemId: item.id, delta: delta),
|
||||
() => barTabService.adjustTabItemQuantity(
|
||||
tabItemId: item.id,
|
||||
delta: delta,
|
||||
order: (orderId: delta > 0 ? _orderFor(item.tabId) : orderId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,6 +216,9 @@ class BarScreenViewModel extends ChangeNotifier {
|
||||
'closeSelectedTab',
|
||||
() => barTabService.closeTab(tab.id, paymentMethod: paymentMethod),
|
||||
);
|
||||
// A paid tab goes back to the Open Tab dialog
|
||||
_openedTabIds.remove(tab.id);
|
||||
deselectTab();
|
||||
}
|
||||
|
||||
Future<T> _guard<T>(String action, Future<T> Function() body) async {
|
||||
|
||||
Reference in New Issue
Block a user