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:
@@ -1,48 +0,0 @@
|
||||
import '../models/closed_tab_item.dart';
|
||||
|
||||
class ClosedTabItemDayGroup {
|
||||
final DateTime day;
|
||||
final List<ClosedTabItem> items;
|
||||
|
||||
const ClosedTabItemDayGroup({required this.day, required this.items});
|
||||
|
||||
int get itemCount => items.fold(
|
||||
0,
|
||||
(total, item) => total + (item.isRemoved ? 0 : item.quantity),
|
||||
);
|
||||
}
|
||||
|
||||
List<ClosedTabItemDayGroup> groupClosedTabItemsByDay(
|
||||
Iterable<ClosedTabItem> items, {
|
||||
required DateTime fallbackDate,
|
||||
}) {
|
||||
final groups = <DateTime, List<ClosedTabItem>>{};
|
||||
|
||||
for (final item in items) {
|
||||
final localPurchasedAt = (item.purchasedAt ?? fallbackDate).toLocal();
|
||||
final day = DateTime(
|
||||
localPurchasedAt.year,
|
||||
localPurchasedAt.month,
|
||||
localPurchasedAt.day,
|
||||
);
|
||||
groups.putIfAbsent(day, () => []).add(item);
|
||||
}
|
||||
|
||||
for (final group in groups.values) {
|
||||
group.sort((a, b) {
|
||||
final aTime = (a.purchasedAt ?? fallbackDate).toLocal();
|
||||
final bTime = (b.purchasedAt ?? fallbackDate).toLocal();
|
||||
final byTime = bTime.compareTo(aTime);
|
||||
if (byTime != 0) return byTime;
|
||||
|
||||
return b.id.compareTo(a.id);
|
||||
});
|
||||
}
|
||||
|
||||
final sortedGroups = groups.entries.toList()
|
||||
..sort((a, b) => b.key.compareTo(a.key));
|
||||
|
||||
return sortedGroups
|
||||
.map((entry) => ClosedTabItemDayGroup(day: entry.key, items: entry.value))
|
||||
.toList();
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/closed_tab.dart';
|
||||
import '../models/closed_tab_item.dart';
|
||||
import '../models/tab_item.dart';
|
||||
|
||||
/// Drinks rung up without an order (before orders were tracked) that follow
|
||||
/// each other this closely form one order, as on the server.
|
||||
const orderGap = Duration(minutes: 5);
|
||||
|
||||
/// One drink, for grouping: [line] is what it is shown as, [lineKey] which
|
||||
/// drinks are counted on one row.
|
||||
class OrderDrink<L> {
|
||||
final L line;
|
||||
final Object lineKey;
|
||||
final String id;
|
||||
final DateTime addedAt;
|
||||
final String? orderId;
|
||||
final bool isRemoved;
|
||||
|
||||
const OrderDrink({
|
||||
required this.line,
|
||||
required this.lineKey,
|
||||
required this.id,
|
||||
required this.addedAt,
|
||||
required this.orderId,
|
||||
required this.isRemoved,
|
||||
});
|
||||
}
|
||||
|
||||
/// The drinks of one line within an order.
|
||||
class TabOrderRow<L> {
|
||||
final L line;
|
||||
|
||||
/// Still on the tab (or paid for, in history).
|
||||
final int quantity;
|
||||
|
||||
/// Taken off again.
|
||||
final int removed;
|
||||
|
||||
const TabOrderRow({
|
||||
required this.line,
|
||||
required this.quantity,
|
||||
required this.removed,
|
||||
});
|
||||
}
|
||||
|
||||
/// Drinks ordered together, shown under one "12:32 today" divider.
|
||||
class TabOrder<L> {
|
||||
/// Null for drinks from before orders, grouped by time instead.
|
||||
final String? orderId;
|
||||
|
||||
/// When its first drink was rung up.
|
||||
final DateTime startedAt;
|
||||
final List<TabOrderRow<L>> rows;
|
||||
|
||||
const TabOrder({
|
||||
required this.orderId,
|
||||
required this.startedAt,
|
||||
required this.rows,
|
||||
});
|
||||
|
||||
int get itemCount => rows.fold(0, (total, row) => total + row.quantity);
|
||||
}
|
||||
|
||||
/// Splits [drinks] into their orders, oldest first. Drinks rung up at the
|
||||
/// same moment keep the order they are given in.
|
||||
List<TabOrder<L>> groupOrders<L>(Iterable<OrderDrink<L>> drinks) {
|
||||
final sorted = drinks.toList();
|
||||
mergeSort(sorted, compare: (a, b) => a.addedAt.compareTo(b.addedAt));
|
||||
|
||||
final orders = <_OrderBuilder<L>>[];
|
||||
final byId = <String, _OrderBuilder<L>>{};
|
||||
_OrderBuilder<L>? unordered;
|
||||
DateTime? unorderedLastAt;
|
||||
|
||||
for (final drink in sorted) {
|
||||
final _OrderBuilder<L> order;
|
||||
final orderId = drink.orderId;
|
||||
if (orderId != null) {
|
||||
order = byId.putIfAbsent(orderId, () {
|
||||
final created = _OrderBuilder<L>(orderId, drink.addedAt);
|
||||
orders.add(created);
|
||||
return created;
|
||||
});
|
||||
} else {
|
||||
if (unordered == null ||
|
||||
drink.addedAt.difference(unorderedLastAt!) > orderGap) {
|
||||
unordered = _OrderBuilder<L>(null, drink.addedAt);
|
||||
orders.add(unordered);
|
||||
}
|
||||
unorderedLastAt = drink.addedAt;
|
||||
order = unordered;
|
||||
}
|
||||
order.add(drink);
|
||||
}
|
||||
|
||||
// Started in the order of their first drink: already oldest first
|
||||
return [for (final order in orders) order.build()];
|
||||
}
|
||||
|
||||
class _OrderBuilder<L> {
|
||||
final String? orderId;
|
||||
final DateTime startedAt;
|
||||
final _rows = <Object, ({L line, int quantity, int removed})>{};
|
||||
|
||||
_OrderBuilder(this.orderId, this.startedAt);
|
||||
|
||||
void add(OrderDrink<L> drink) {
|
||||
final row = _rows[drink.lineKey];
|
||||
_rows[drink.lineKey] = (
|
||||
line: row?.line ?? drink.line,
|
||||
quantity: (row?.quantity ?? 0) + (drink.isRemoved ? 0 : 1),
|
||||
removed: (row?.removed ?? 0) + (drink.isRemoved ? 1 : 0),
|
||||
);
|
||||
}
|
||||
|
||||
TabOrder<L> build() => TabOrder(
|
||||
orderId: orderId,
|
||||
startedAt: startedAt,
|
||||
rows: [
|
||||
for (final row in _rows.values)
|
||||
TabOrderRow(
|
||||
line: row.line,
|
||||
quantity: row.quantity,
|
||||
removed: row.removed,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The orders of an open tab. Without [includeRemoved] only what is still on
|
||||
/// it: rows and orders whose drinks were all taken off again are left out.
|
||||
/// Drinks of an item without purchase records count as ordered when the tab
|
||||
/// was opened.
|
||||
List<TabOrder<TabItem>> ordersOfTab(BarTab tab, {bool includeRemoved = false}) {
|
||||
final orders = groupOrders([
|
||||
for (final item in tab.items)
|
||||
if (item.purchases.isEmpty)
|
||||
for (var index = 0; index < item.quantity; index++)
|
||||
OrderDrink(
|
||||
line: item,
|
||||
lineKey: item.id,
|
||||
id: '${item.id}-$index',
|
||||
addedAt: tab.openedAt,
|
||||
orderId: null,
|
||||
isRemoved: false,
|
||||
)
|
||||
else
|
||||
for (final purchase in item.purchases)
|
||||
OrderDrink(
|
||||
line: item,
|
||||
lineKey: item.id,
|
||||
id: purchase.id,
|
||||
addedAt: purchase.purchasedAt,
|
||||
orderId: purchase.orderId,
|
||||
isRemoved: purchase.isRemoved,
|
||||
),
|
||||
]);
|
||||
if (includeRemoved) return orders;
|
||||
|
||||
return [
|
||||
for (final order in orders)
|
||||
if (order.itemCount > 0)
|
||||
TabOrder(
|
||||
orderId: order.orderId,
|
||||
startedAt: order.startedAt,
|
||||
rows: [
|
||||
for (final row in order.rows)
|
||||
if (row.quantity > 0) row,
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// The orders of a paid tab, with the drinks taken off before paying.
|
||||
List<TabOrder<ClosedTabItem>> ordersOfClosedTab(ClosedTab tab) {
|
||||
return groupOrders([
|
||||
// The items come newest first; drinks of the same moment keep their
|
||||
// order, so start from the oldest
|
||||
for (final item in tab.items.reversed)
|
||||
for (var index = 0; index < item.quantity; index++)
|
||||
OrderDrink(
|
||||
line: item,
|
||||
lineKey: (item.productId, item.productName, item.unitPriceInCents),
|
||||
id: '${item.id}-$index',
|
||||
addedAt: item.purchasedAt ?? tab.closedAt,
|
||||
orderId: item.orderId,
|
||||
isRemoved: item.isRemoved,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// "12:32 today", "12:32 yesterday", or the date with the time.
|
||||
String orderTimeLabel(
|
||||
AppLocalizations l10n,
|
||||
String locale,
|
||||
DateTime startedAt, {
|
||||
DateTime? now,
|
||||
}) {
|
||||
final local = startedAt.toLocal();
|
||||
final today = now ?? DateTime.now();
|
||||
final day = DateTime(local.year, local.month, local.day);
|
||||
final days = DateTime(
|
||||
today.year,
|
||||
today.month,
|
||||
today.day,
|
||||
).difference(day).inDays;
|
||||
final time = DateFormat.Hm(locale).format(local);
|
||||
|
||||
return switch (days) {
|
||||
0 => l10n.orderStartedToday(time),
|
||||
1 => l10n.orderStartedYesterday(time),
|
||||
_ => l10n.orderStartedOn(DateFormat.yMMMd(locale).format(local), time),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user