feat: add more detailed history and group per day

This commit is contained in:
2026-08-07 05:07:41 +02:00
parent 1dd7e69aa8
commit 799c1c4045
23 changed files with 2506 additions and 346 deletions
+167 -26
View File
@@ -8,10 +8,11 @@ import '../models/closed_tab_item.dart';
import '../models/payment_method.dart';
import '../models/product.dart';
import '../models/tab_item.dart';
import '../models/tab_item_purchase.dart';
import 'product_service.dart';
abstract class BarTabService {
Future<List<BarTab>> getOpenTabs();
Future<List<BarTab>> getOpenTabs({bool includeRemoved = false});
Future<BarTab?> getTabById(String id);
@@ -58,7 +59,16 @@ class DriftBarTabService implements BarTabService {
DriftBarTabService({required this.database});
TabItem _mapItemRow(TabItemRow row) {
TabItemPurchase _mapPurchaseRow(TabItemPurchaseRow row) {
return TabItemPurchase(
id: row.id,
tabItemId: row.tabItemId,
purchasedAt: row.purchasedAt,
removedAt: row.removedAt,
);
}
TabItem _mapItemRow(TabItemRow row, List<TabItemPurchase> purchases) {
return TabItem(
id: row.id,
tabId: row.tabId,
@@ -66,6 +76,7 @@ class DriftBarTabService implements BarTabService {
productName: row.productName,
quantity: row.quantity,
unitPriceInCents: row.unitPriceInCents,
purchases: purchases,
);
}
@@ -88,6 +99,8 @@ class DriftBarTabService implements BarTabService {
productName: row.productName,
quantity: row.quantity,
unitPriceInCents: row.unitPriceInCents,
purchasedAt: row.purchasedAt,
removedAt: row.removedAt,
);
}
@@ -98,7 +111,97 @@ class DriftBarTabService implements BarTabService {
final rows = await query.get();
return rows.map(_mapItemRow).toList();
final items = <TabItem>[];
for (final row in rows) {
if (row.quantity <= 0) continue;
final purchases = await _getPurchaseRecords(row);
items.add(_mapItemRow(row, purchases));
}
return items;
}
Future<List<TabItem>> _getItemsForHistory(String tabId) async {
final query = database.select(database.tabItems)
..where((item) => item.tabId.equals(tabId))
..orderBy([(item) => OrderingTerm.asc(item.createdAt)]);
final rows = await query.get();
final items = <TabItem>[];
for (final row in rows) {
final purchases = await _getPurchaseRecords(row);
if (purchases.isEmpty) continue;
items.add(_mapItemRow(row, purchases));
}
return items;
}
Future<List<TabItemPurchaseRow>> _getPurchaseRows(String tabItemId) async {
final query = database.select(database.tabItemPurchases)
..where((purchase) => purchase.tabItemId.equals(tabItemId))
..orderBy([
(purchase) => OrderingTerm.asc(purchase.purchasedAt),
(purchase) => OrderingTerm.asc(purchase.id),
]);
return query.get();
}
Future<List<TabItemPurchase>> _getPurchaseRecords(TabItemRow item) async {
final rows = await _getPurchaseRows(item.id);
final purchases = rows.map(_mapPurchaseRow).toList();
final activeCount = purchases
.where((purchase) => !purchase.isRemoved)
.length;
final missingActivePurchases = item.quantity - activeCount;
if (missingActivePurchases <= 0) return purchases;
return [
...purchases,
for (var index = 0; index < missingActivePurchases; index++)
TabItemPurchase(
id: 'legacy-${item.id}-$index',
tabItemId: item.id,
purchasedAt: item.createdAt,
),
];
}
Future<List<TabItemPurchaseRow>> _ensurePurchaseRows(TabItemRow item) async {
final rows = await _getPurchaseRows(item.id);
final activeCount = rows
.where((purchase) => purchase.removedAt == null)
.length;
final missingActivePurchases = item.quantity - activeCount;
for (var index = 0; index < missingActivePurchases; index++) {
await _recordPurchase(tabItemId: item.id, purchasedAt: item.createdAt);
}
if (missingActivePurchases <= 0) return rows;
return _getPurchaseRows(item.id);
}
Future<void> _recordPurchase({
required String tabItemId,
DateTime? purchasedAt,
}) async {
await database
.into(database.tabItemPurchases)
.insert(
TabItemPurchasesCompanion.insert(
id: _uuid.v4(),
tabItemId: tabItemId,
purchasedAt: purchasedAt ?? DateTime.now(),
),
);
}
Future<void> _decreaseProductStock(String productId, int amount) async {
@@ -150,7 +253,7 @@ class DriftBarTabService implements BarTabService {
}
@override
Future<List<BarTab>> getOpenTabs() async {
Future<List<BarTab>> getOpenTabs({bool includeRemoved = false}) async {
final query = database.select(database.barTabs)
..where((tab) => tab.status.equals('open'))
..orderBy([(tab) => OrderingTerm.desc(tab.openedAt)]);
@@ -160,7 +263,9 @@ class DriftBarTabService implements BarTabService {
final tabs = <BarTab>[];
for (final tabRow in tabRows) {
final items = await _getItemsForTab(tabRow.id);
final items = includeRemoved
? await _getItemsForHistory(tabRow.id)
: await _getItemsForTab(tabRow.id);
tabs.add(_mapTabRow(tabRow, items));
}
@@ -286,15 +391,18 @@ class DriftBarTabService implements BarTabService {
await updateQuery.write(
TabItemsCompanion(quantity: Value(existingItem.quantity + 1)),
);
await _recordPurchase(tabItemId: existingItem.id);
return;
}
final tabItemId = _uuid.v4();
await database
.into(database.tabItems)
.insert(
TabItemsCompanion.insert(
id: _uuid.v4(),
id: tabItemId,
tabId: tabId,
productId: product.id,
productName: product.name,
@@ -303,6 +411,8 @@ class DriftBarTabService implements BarTabService {
createdAt: DateTime.now(),
),
);
await _recordPurchase(tabItemId: tabItemId);
});
}
@@ -316,24 +426,47 @@ class DriftBarTabService implements BarTabService {
database.tabItems,
)..where((row) => row.id.equals(tabItemId))).getSingleOrNull();
if (item == null || delta == 0) return 0;
if (item == null || delta == 0 || (item.quantity == 0 && delta < 0)) {
return 0;
}
final requestedQuantity = item.quantity + delta;
final actualDelta = requestedQuantity <= 0 ? -item.quantity : delta;
if (actualDelta > 0) {
await _decreaseProductStock(item.productId, actualDelta);
for (var index = 0; index < actualDelta; index++) {
await _recordPurchase(tabItemId: item.id);
}
} else {
await _increaseProductStock(item.productId, -actualDelta);
final purchaseRows = await _ensurePurchaseRows(item);
final activePurchaseRows =
purchaseRows
.where((purchase) => purchase.removedAt == null)
.toList()
..sort((a, b) => b.purchasedAt.compareTo(a.purchasedAt));
final removedAt = DateTime.now();
for (final purchase in activePurchaseRows.take(-actualDelta)) {
await (database.update(database.tabItemPurchases)
..where((row) => row.id.equals(purchase.id)))
.write(TabItemPurchasesCompanion(removedAt: Value(removedAt)));
}
}
if (requestedQuantity <= 0) {
final deletedRows = await (database.delete(
database.tabItems,
)..where((row) => row.id.equals(tabItemId))).go();
final updatedRows =
await (database.update(database.tabItems)
..where((row) => row.id.equals(tabItemId)))
.write(const TabItemsCompanion(quantity: Value(0)));
if (deletedRows != 1) {
throw StateError('Tab item was changed before it could be deleted');
if (updatedRows != 1) {
throw StateError(
'Tab item was changed before its quantity was updated',
);
}
} else {
final updatedRows =
@@ -367,7 +500,7 @@ class DriftBarTabService implements BarTabService {
return;
}
final items = await _getItemsForTab(tabId);
final items = await _getItemsForHistory(tabId);
if (items.isEmpty) {
return;
@@ -388,18 +521,22 @@ class DriftBarTabService implements BarTabService {
);
for (final item in items) {
await database
.into(database.closedTabItems)
.insert(
ClosedTabItemsCompanion.insert(
id: _uuid.v4(),
closedTabId: closedTabId,
productId: item.productId,
productName: item.productName,
quantity: item.quantity,
unitPriceInCents: item.unitPriceInCents,
),
);
for (final purchase in item.purchases) {
await database
.into(database.closedTabItems)
.insert(
ClosedTabItemsCompanion.insert(
id: _uuid.v4(),
closedTabId: closedTabId,
productId: item.productId,
productName: item.productName,
quantity: 1,
unitPriceInCents: item.unitPriceInCents,
purchasedAt: Value(purchase.purchasedAt),
removedAt: Value(purchase.removedAt),
),
);
}
}
final deleteQuery = database.delete(database.tabItems)
@@ -437,7 +574,11 @@ class DriftBarTabService implements BarTabService {
for (final row in closedTabRows) {
final itemsQuery = database.select(database.closedTabItems)
..where((item) => item.closedTabId.equals(row.id));
..where((item) => item.closedTabId.equals(row.id))
..orderBy([
(item) => OrderingTerm.desc(item.purchasedAt),
(item) => OrderingTerm.desc(item.id),
]);
final itemRows = await itemsQuery.get();
+63 -30
View File
@@ -49,19 +49,27 @@ class DefaultExportService implements ExportService {
final itemRows = await itemsQuery.get();
result.add(_ClosedTabExport(
id: tabRow.id,
originalTabId: tabRow.originalTabId,
customerName: tabRow.customerName,
closedAt: tabRow.closedAt.toIso8601String(),
paymentMethod: tabRow.paymentMethod,
items: itemRows.map((row) => _ClosedTabItemExport(
productId: row.productId,
productName: row.productName,
quantity: row.quantity,
unitPriceInCents: row.unitPriceInCents,
)).toList(),
));
result.add(
_ClosedTabExport(
id: tabRow.id,
originalTabId: tabRow.originalTabId,
customerName: tabRow.customerName,
closedAt: tabRow.closedAt.toIso8601String(),
paymentMethod: tabRow.paymentMethod,
items: itemRows
.map(
(row) => _ClosedTabItemExport(
productId: row.productId,
productName: row.productName,
quantity: row.quantity,
unitPriceInCents: row.unitPriceInCents,
purchasedAt: row.purchasedAt?.toIso8601String(),
removedAt: row.removedAt?.toIso8601String(),
),
)
.toList(),
),
);
}
return result;
@@ -71,22 +79,39 @@ class DefaultExportService implements ExportService {
return {
'exportedAt': DateTime.now().toIso8601String(),
'appVersion': '1.0.7',
'closedTabs': tabs.map((tab) => {
'id': tab.id,
'originalTabId': tab.originalTabId,
'customerName': tab.customerName,
'closedAt': tab.closedAt,
'paymentMethod': tab.paymentMethod,
'items': tab.items.map((item) => {
'productId': item.productId,
'productName': item.productName,
'quantity': item.quantity,
'unitPriceInCents': item.unitPriceInCents,
'lineTotalInCents': item.quantity * item.unitPriceInCents,
}).toList(),
'itemCount': tab.items.fold(0, (sum, item) => sum + item.quantity),
'totalInCents': tab.items.fold(0, (sum, item) => sum + item.quantity * item.unitPriceInCents),
}).toList(),
'closedTabs': tabs
.map(
(tab) => {
'id': tab.id,
'originalTabId': tab.originalTabId,
'customerName': tab.customerName,
'closedAt': tab.closedAt,
'paymentMethod': tab.paymentMethod,
'items': tab.items
.map(
(item) => {
'productId': item.productId,
'productName': item.productName,
'quantity': item.quantity,
'unitPriceInCents': item.unitPriceInCents,
'purchasedAt': item.purchasedAt,
'removedAt': item.removedAt,
'isRemoved': item.isRemoved,
'lineTotalInCents': item.lineTotalInCents,
},
)
.toList(),
'itemCount': tab.items.fold(
0,
(sum, item) => sum + (item.isRemoved ? 0 : item.quantity),
),
'totalInCents': tab.items.fold(
0,
(sum, item) => sum + item.lineTotalInCents,
),
},
)
.toList(),
};
}
}
@@ -96,13 +121,21 @@ class _ClosedTabItemExport {
final String productName;
final int quantity;
final int unitPriceInCents;
final String? purchasedAt;
final String? removedAt;
_ClosedTabItemExport({
required this.productId,
required this.productName,
required this.quantity,
required this.unitPriceInCents,
required this.purchasedAt,
required this.removedAt,
});
bool get isRemoved => removedAt != null;
int get lineTotalInCents => isRemoved ? 0 : quantity * unitPriceInCents;
}
class _ClosedTabExport {
@@ -121,4 +154,4 @@ class _ClosedTabExport {
required this.paymentMethod,
required this.items,
});
}
}