import 'package:drift/drift.dart'; import 'package:uuid/uuid.dart'; import '../database/app_database.dart'; import '../models/bar_tab.dart'; import '../models/closed_tab.dart'; import '../models/closed_tab_item.dart'; import '../models/product.dart'; import '../models/tab_item.dart'; abstract class BarTabService { Future> getOpenTabs(); Future getTabById(String id); Future createTab({ required String customerName, }); Future addProductToTab({ required String tabId, required Product product, }); Future updateTabItemQuantity({ required String tabItemId, required int quantity, }); /// Archives the tab's current items into history and clears them. /// The tab itself stays open under the same customer name. Future closeTab(String tabId); Future> getClosedTabs(); } class DriftBarTabService implements BarTabService { final AppDatabase database; final _uuid = const Uuid(); DriftBarTabService({ required this.database, }); TabItem _mapItemRow(TabItemRow row) { return TabItem( id: row.id, tabId: row.tabId, productId: row.productId, productName: row.productName, quantity: row.quantity, unitPriceInCents: row.unitPriceInCents, ); } BarTab _mapTabRow( BarTabRow row, List items, ) { return BarTab( id: row.id, customerName: row.customerName, status: row.status, openedAt: row.openedAt, closedAt: row.closedAt, items: items, ); } ClosedTabItem _mapClosedItemRow(ClosedTabItemRow row) { return ClosedTabItem( id: row.id, closedTabId: row.closedTabId, productId: row.productId, productName: row.productName, quantity: row.quantity, unitPriceInCents: row.unitPriceInCents, ); } Future> _getItemsForTab(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(); return rows.map(_mapItemRow).toList(); } @override Future> getOpenTabs() async { final query = database.select(database.barTabs) ..where((tab) => tab.status.equals('open')) ..orderBy([ (tab) => OrderingTerm.desc(tab.openedAt), ]); final tabRows = await query.get(); final tabs = []; for (final tabRow in tabRows) { final items = await _getItemsForTab(tabRow.id); tabs.add(_mapTabRow(tabRow, items)); } return tabs; } @override Future getTabById(String id) async { final query = database.select(database.barTabs) ..where((tab) => tab.id.equals(id)); final tabRow = await query.getSingleOrNull(); if (tabRow == null) { return null; } final items = await _getItemsForTab(tabRow.id); return _mapTabRow(tabRow, items); } @override Future createTab({ required String customerName, }) async { final id = _uuid.v4(); await database.into(database.barTabs).insert( BarTabsCompanion.insert( id: id, customerName: customerName, status: const Value('open'), openedAt: DateTime.now(), ), ); final tab = await getTabById(id); if (tab == null) { throw Exception('Could not create tab.'); } return tab; } @override Future addProductToTab({ required String tabId, required Product product, }) async { await database.transaction(() async { final existingItemQuery = database.select(database.tabItems) ..where( (item) => item.tabId.equals(tabId) & item.productId.equals(product.id), ); final existingItem = await existingItemQuery.getSingleOrNull(); if (existingItem != null) { final updateQuery = database.update(database.tabItems) ..where((item) => item.id.equals(existingItem.id)); await updateQuery.write( TabItemsCompanion( quantity: Value(existingItem.quantity + 1), ), ); return; } await database.into(database.tabItems).insert( TabItemsCompanion.insert( id: _uuid.v4(), tabId: tabId, productId: product.id, productName: product.name, quantity: 1, unitPriceInCents: product.priceInCents, createdAt: DateTime.now(), ), ); }); } @override Future updateTabItemQuantity({ required String tabItemId, required int quantity, }) async { if (quantity <= 0) { final deleteQuery = database.delete(database.tabItems) ..where((item) => item.id.equals(tabItemId)); await deleteQuery.go(); return; } final updateQuery = database.update(database.tabItems) ..where((item) => item.id.equals(tabItemId)); await updateQuery.write( TabItemsCompanion( quantity: Value(quantity), ), ); } @override Future closeTab(String tabId) async { await database.transaction(() async { final tabQuery = database.select(database.barTabs) ..where((tab) => tab.id.equals(tabId)); final tabRow = await tabQuery.getSingleOrNull(); if (tabRow == null) { return; } final items = await _getItemsForTab(tabId); if (items.isEmpty) { return; } final closedTabId = _uuid.v4(); await database.into(database.closedTabs).insert( ClosedTabsCompanion.insert( id: closedTabId, originalTabId: tabId, customerName: tabRow.customerName, closedAt: DateTime.now(), ), ); 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, ), ); } final deleteQuery = database.delete(database.tabItems) ..where((item) => item.tabId.equals(tabId)); await deleteQuery.go(); }); } @override Future> getClosedTabs() async { final query = database.select(database.closedTabs) ..orderBy([ (tab) => OrderingTerm.desc(tab.closedAt), ]); final closedTabRows = await query.get(); final closedTabs = []; for (final row in closedTabRows) { final itemsQuery = database.select(database.closedTabItems) ..where((item) => item.closedTabId.equals(row.id)); final itemRows = await itemsQuery.get(); closedTabs.add(ClosedTab( id: row.id, originalTabId: row.originalTabId, customerName: row.customerName, closedAt: row.closedAt, items: itemRows.map(_mapClosedItemRow).toList(), )); } return closedTabs; } }