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:
2026-10-28 02:11:32 +01:00
parent 1aee0541c4
commit 29ceb51177
41 changed files with 2771 additions and 812 deletions
+11 -1
View File
@@ -5,6 +5,7 @@ import 'tab_item.dart';
class BarTab {
final String id;
final String customerName;
final String? email;
final DateTime openedAt;
final int version;
final List<TabItem> items;
@@ -12,6 +13,7 @@ class BarTab {
const BarTab({
required this.id,
required this.customerName,
this.email,
required this.openedAt,
this.version = 0,
required this.items,
@@ -31,11 +33,19 @@ class BarTab {
other is BarTab &&
id == other.id &&
customerName == other.customerName &&
email == other.email &&
openedAt == other.openedAt &&
version == other.version &&
listEquals(items, other.items);
@override
int get hashCode =>
Object.hash(id, customerName, openedAt, version, Object.hashAll(items));
Object.hash(
id,
customerName,
email,
openedAt,
version,
Object.hashAll(items),
);
}
+5 -1
View File
@@ -7,6 +7,7 @@ class ClosedTabItem {
final int unitPriceInCents;
final DateTime? purchasedAt;
final DateTime? removedAt;
final String? orderId;
ClosedTabItem({
required this.id,
@@ -17,6 +18,7 @@ class ClosedTabItem {
required this.unitPriceInCents,
this.purchasedAt,
this.removedAt,
this.orderId,
});
bool get isRemoved => removedAt != null;
@@ -34,7 +36,8 @@ class ClosedTabItem {
quantity == other.quantity &&
unitPriceInCents == other.unitPriceInCents &&
purchasedAt == other.purchasedAt &&
removedAt == other.removedAt;
removedAt == other.removedAt &&
orderId == other.orderId;
@override
int get hashCode => Object.hash(
@@ -46,5 +49,6 @@ class ClosedTabItem {
unitPriceInCents,
purchasedAt,
removedAt,
orderId,
);
}
+13 -1
View File
@@ -8,7 +8,15 @@ class ServerUnit {
final DateTime addedAt;
final DateTime? removedAt;
const ServerUnit({required this.id, required this.addedAt, this.removedAt});
/// The order it was rung up in; null for drinks from older app versions.
final String? orderId;
const ServerUnit({
required this.id,
required this.addedAt,
this.removedAt,
this.orderId,
});
factory ServerUnit.fromJson(Map<String, dynamic> json) {
return ServerUnit(
@@ -18,6 +26,7 @@ class ServerUnit {
final String value => DateTime.parse(value).toLocal(),
_ => null,
},
orderId: json['orderId'] as String?,
);
}
}
@@ -58,6 +67,7 @@ class ServerTabLine {
class ServerTab {
final String id;
final String customerName;
final String? email;
final DateTime openedAt;
final int version;
final List<ServerTabLine> lines;
@@ -65,6 +75,7 @@ class ServerTab {
const ServerTab({
required this.id,
required this.customerName,
this.email,
required this.openedAt,
required this.version,
this.lines = const [],
@@ -74,6 +85,7 @@ class ServerTab {
return ServerTab(
id: json['id'] as String,
customerName: json['customerName'] as String,
email: json['email'] as String?,
openedAt: DateTime.parse(json['openedAt'] as String).toLocal(),
version: json['version'] as int? ?? 0,
lines: [
+22 -3
View File
@@ -47,6 +47,10 @@ class TabEvent {
const TabEvent({required this.id, required this.payload});
}
/// The order a removal takes drinks off: `orderId`, or (when null) the
/// drinks rung up before orders were tracked.
typedef OrderRef = ({String? orderId});
/// One settled line in a `settled` event.
typedef SettledLine = ({
String productId,
@@ -66,12 +70,16 @@ class TabEventFactory {
required String tabId,
required String customerName,
required DateTime openedAt,
String? email,
}) {
return _event(
TabEventType.tabOpened,
tabId: tabId,
customerName: customerName,
fields: {'openedAt': openedAt.toUtc().toIso8601String()},
fields: {
'openedAt': openedAt.toUtc().toIso8601String(),
'email': ?email,
},
);
}
@@ -96,7 +104,8 @@ class TabEventFactory {
}
/// [quantity] is how many pieces were added; always positive. [lineId] is
/// the local line, so the server can keep its id.
/// the local line, so the server can keep its id. [orderId] is the order
/// they are part of.
TabEvent lineAdded({
required String tabId,
required String customerName,
@@ -105,6 +114,7 @@ class TabEventFactory {
required String productName,
required int unitPriceInCents,
required int quantity,
required String? orderId,
}) {
return _lineEvent(
TabEventType.lineAdded,
@@ -115,10 +125,13 @@ class TabEventFactory {
productName: productName,
unitPriceInCents: unitPriceInCents,
quantity: quantity,
order: (orderId: orderId),
);
}
/// [quantity] is how many pieces were removed; always positive.
/// [quantity] is how many pieces were removed; always positive. With
/// [order] they are taken off that order only; without, the latest drinks
/// of the line (as before orders).
TabEvent lineRemoved({
required String tabId,
required String customerName,
@@ -127,6 +140,7 @@ class TabEventFactory {
required String productName,
required int unitPriceInCents,
required int quantity,
OrderRef? order,
}) {
return _lineEvent(
TabEventType.lineRemoved,
@@ -137,6 +151,7 @@ class TabEventFactory {
productName: productName,
unitPriceInCents: unitPriceInCents,
quantity: quantity,
order: order,
);
}
@@ -246,6 +261,7 @@ class TabEventFactory {
required String productName,
required int unitPriceInCents,
required int quantity,
required OrderRef? order,
}) {
if (quantity < 1) {
throw ArgumentError.value(quantity, 'quantity', 'Must be at least 1');
@@ -263,6 +279,9 @@ class TabEventFactory {
'productName': productName,
'unitPriceInCents': unitPriceInCents,
'quantity': quantity,
// Also when null: a removal then only takes drinks without an order,
// where a removal without `orderId` takes the latest of the line
if (order != null) 'orderId': order.orderId,
},
);
}
+8 -2
View File
@@ -4,11 +4,15 @@ class TabItemPurchase {
final DateTime purchasedAt;
final DateTime? removedAt;
/// The order it was rung up in; null for drinks from before orders.
final String? orderId;
const TabItemPurchase({
required this.id,
required this.tabItemId,
required this.purchasedAt,
this.removedAt,
this.orderId,
});
bool get isRemoved => removedAt != null;
@@ -20,8 +24,10 @@ class TabItemPurchase {
id == other.id &&
tabItemId == other.tabItemId &&
purchasedAt == other.purchasedAt &&
removedAt == other.removedAt;
removedAt == other.removedAt &&
orderId == other.orderId;
@override
int get hashCode => Object.hash(id, tabItemId, purchasedAt, removedAt);
int get hashCode =>
Object.hash(id, tabItemId, purchasedAt, removedAt, orderId);
}