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
+80 -3
View File
@@ -196,6 +196,29 @@ void main() {
expect(updatedTab.items.first.quantity, 3);
});
test('records each addition as a separate purchase event', () async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct();
await service.addProductToTab(tabId: tab.id, product: product);
await service.addProductToTab(tabId: tab.id, product: product);
final barTab = (await service.getOpenTabs()).first;
final historyTab = (await service.getOpenTabs(
includeRemoved: true,
)).first;
expect(barTab.items.first.quantity, 2);
expect(barTab.items.first.purchases, hasLength(2));
expect(historyTab.items.first.purchases, hasLength(2));
expect(
historyTab.items.first.purchases.every(
(purchase) => !purchase.isRemoved,
),
isTrue,
);
});
test('adding product calculates correct line total', () async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct(priceInCents: 450);
@@ -251,7 +274,7 @@ void main() {
expect(updatedTab!.items.first.quantity, 5);
});
test('deletes item when the delta removes all quantity', () async {
test('hides item when the delta removes all quantity', () async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct();
await service.addProductToTab(tabId: tab.id, product: product);
@@ -282,6 +305,32 @@ void main() {
expect(updatedProduct.stockQuantity, 100);
});
test('keeps a removed purchase in the history audit trail', () async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct();
await service.addProductToTab(tabId: tab.id, product: product);
await service.addProductToTab(tabId: tab.id, product: product);
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1);
final historyTab = (await service.getOpenTabs(
includeRemoved: true,
)).first;
final item = historyTab.items.first;
expect(item.quantity, 1);
expect(
item.purchases.where((purchase) => purchase.isRemoved),
hasLength(1),
);
expect(
item.purchases.where((purchase) => !purchase.isRemoved),
hasLength(1),
);
});
test(
'applies concurrent deltas without losing stock consistency',
() async {
@@ -320,10 +369,38 @@ void main() {
final closedTabs = await service.getClosedTabs();
expect(closedTabs.length, 1);
expect(closedTabs.first.customerName, 'John');
expect(closedTabs.first.items.length, 1);
expect(closedTabs.first.items.first.quantity, 2);
expect(closedTabs.first.items.length, 2);
expect(
closedTabs.first.items.every((item) => item.quantity == 1),
true,
);
expect(
closedTabs.first.items.every((item) => item.purchasedAt != null),
true,
);
});
test(
'keeps removed purchases visible but excludes them from totals',
() async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct();
await service.addProductToTab(tabId: tab.id, product: product);
await service.addProductToTab(tabId: tab.id, product: product);
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
await service.adjustTabItemQuantity(tabItemId: tabItemId, delta: -1);
await service.closeTab(tab.id);
final closedTab = (await service.getClosedTabs()).first;
expect(closedTab.items, hasLength(2));
expect(closedTab.items.where((item) => item.isRemoved), hasLength(1));
expect(closedTab.itemCount, 1);
expect(closedTab.totalInCents, 400);
},
);
test('clears tab items after closing', () async {
final tab = await service.createTab(customerName: 'John');
final product = createTestProduct();
+78
View File
@@ -0,0 +1,78 @@
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltab2/database/app_database.dart';
import 'package:kooltab2/models/product.dart';
import 'package:kooltab2/services/bar_tab_service.dart';
import 'package:kooltab2/services/pin_lock_service.dart';
import 'package:kooltab2/services/product_service.dart';
import 'package:kooltab2/services/settings_service.dart';
import 'package:kooltab2/viewmodels/dev_menu_view_model.dart';
import 'package:mocktail/mocktail.dart';
class MockBarTabService extends Mock implements BarTabService {}
class MockPinLockService extends Mock implements PinLockService {}
class MockProductService extends Mock implements ProductService {}
class MockSettingsService extends Mock implements SettingsService {}
void main() {
late AppDatabase database;
late MockProductService productService;
late DevMenuViewModel viewModel;
setUp(() {
database = AppDatabase(NativeDatabase.memory());
productService = MockProductService();
when(() => productService.getProducts()).thenAnswer(
(_) async => const [
Product(
id: 'product-1',
name: 'Cola',
category: 'Drinks',
stockQuantity: 100,
lowStockThreshold: 10,
priceInCents: 250,
),
Product(
id: 'product-2',
name: 'Chips',
category: 'Snacks',
stockQuantity: 100,
lowStockThreshold: 10,
priceInCents: 150,
),
],
);
viewModel = DevMenuViewModel(
database: database,
productService: productService,
barTabService: MockBarTabService(),
settingsService: MockSettingsService(),
pinLockService: MockPinLockService(),
);
});
tearDown(() async {
await database.close();
});
test('generates individual timestamped mock history items', () async {
await viewModel.generateMockOrders(count: 12);
final tabs = await database.select(database.closedTabs).get();
final items = await database.select(database.closedTabItems).get();
expect(tabs, hasLength(12));
expect(items, isNotEmpty);
expect(items.every((item) => item.quantity == 1), isTrue);
expect(items.every((item) => item.purchasedAt != null), isTrue);
expect(
viewModel.lastAction,
startsWith('Generated 12 mock orders across '),
);
});
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltab2/models/closed_tab_item.dart';
import 'package:kooltab2/utils/history_grouping.dart';
void main() {
test('groups items by purchase day with newest day first', () {
final items = [
_closedItem('older', DateTime(2026, 8, 1, 22)),
_closedItem('newer-b', DateTime(2026, 8, 3, 18)),
_closedItem('newer-a', DateTime(2026, 8, 3, 10)),
];
final groups = groupClosedTabItemsByDay(
items,
fallbackDate: DateTime(2026, 8, 4),
);
expect(groups, hasLength(2));
expect(groups.first.day, DateTime(2026, 8, 3));
expect(groups.first.items.map((item) => item.id), ['newer-b', 'newer-a']);
expect(groups.last.day, DateTime(2026, 8, 1));
});
}
ClosedTabItem _closedItem(String id, DateTime purchasedAt) {
return ClosedTabItem(
id: id,
closedTabId: 'closed-tab',
productId: 'product-$id',
productName: id,
quantity: 1,
unitPriceInCents: 100,
purchasedAt: purchasedAt,
);
}
+108
View File
@@ -0,0 +1,108 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltab2/models/bar_tab.dart';
import 'package:kooltab2/models/closed_tab.dart';
import 'package:kooltab2/models/payment_method.dart';
import 'package:kooltab2/services/bar_tab_service.dart';
import 'package:kooltab2/viewmodels/history_view_model.dart';
import 'package:mocktail/mocktail.dart';
class MockBarTabService extends Mock implements BarTabService {}
void main() {
late MockBarTabService service;
late HistoryViewModel viewModel;
setUp(() {
service = MockBarTabService();
viewModel = HistoryViewModel(barTabService: service);
when(
() => service.getClosedTabsPaginated(
limit: 20,
offset: 0,
customerName: null,
),
).thenAnswer((_) async => []);
when(
() => service.getClosedTabCount(customerName: null),
).thenAnswer((_) async => 0);
when(() => service.getDistinctCustomerNames()).thenAnswer((_) async => []);
when(
() => service.getOpenTabs(includeRemoved: true),
).thenAnswer((_) async => [_openTab]);
});
test('loads open tabs for the top of history', () async {
await viewModel.load();
expect(viewModel.openTabs, hasLength(1));
expect(viewModel.openTabs.first.customerName, 'Alice');
expect(viewModel.customerNames, ['Alice']);
});
test('search filters open tabs as well as closed tabs', () async {
await viewModel.load();
viewModel.search('bob');
expect(viewModel.openTabs, isEmpty);
});
test('sorts closed and open tabs newest first', () async {
final olderClosedTab = _closedTab('closed-old', DateTime(2026, 8, 1));
final newerClosedTab = _closedTab('closed-new', DateTime(2026, 8, 3));
final olderOpenTab = _barTab('open-old', DateTime(2026, 8, 1));
final newerOpenTab = _barTab('open-new', DateTime(2026, 8, 3));
when(
() => service.getClosedTabsPaginated(
limit: 20,
offset: 0,
customerName: null,
),
).thenAnswer((_) async => [olderClosedTab, newerClosedTab]);
when(
() => service.getClosedTabCount(customerName: null),
).thenAnswer((_) async => 2);
when(
() => service.getOpenTabs(includeRemoved: true),
).thenAnswer((_) async => [olderOpenTab, newerOpenTab]);
await viewModel.load();
expect(viewModel.closedTabs.map((tab) => tab.id), [
'closed-new',
'closed-old',
]);
expect(viewModel.openTabs.map((tab) => tab.id), ['open-new', 'open-old']);
});
}
ClosedTab _closedTab(String id, DateTime closedAt) {
return ClosedTab(
id: id,
originalTabId: id,
customerName: id,
closedAt: closedAt,
paymentMethod: PaymentMethod.cash,
items: const [],
);
}
BarTab _barTab(String id, DateTime openedAt) {
return BarTab(
id: id,
customerName: id,
status: 'open',
openedAt: openedAt,
items: const [],
);
}
final _openTab = BarTab(
id: 'tab-1',
customerName: 'Alice',
status: 'open',
openedAt: DateTime(2026, 8, 2, 12),
items: const [],
);