import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:kooltab2/viewmodels/pin_lock_view_model.dart'; import 'package:kooltab2/viewmodels/product_list_view_model.dart'; import 'package:kooltab2/views/dialogs/close_tab_dialog.dart'; import 'package:kooltab2/views/dialogs/new_tab_dialog.dart'; import 'package:kooltab2/views/widgets/product_tile.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import '../models/bar_tab.dart'; import '../models/product.dart'; import '../models/tab_item.dart'; import '../utils/app_updater.dart'; import '../viewmodels/bar_screen_view_model.dart'; class BarScreenView extends StatefulWidget { const BarScreenView({super.key}); @override State createState() => _BarScreenViewState(); } class _BarScreenViewState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { UpdateChecker.check(context); }); } @override Widget build(BuildContext context) { final viewModel = context.watch(); final productsViewModel = context.watch(); return Scaffold( appBar: AppBar( title: const Text('Bar Tabs'), actions: [ IconButton( tooltip: 'Manage products', onPressed: () => context.go('/products'), icon: const Icon(Icons.inventory_2_outlined), ), const SizedBox(width: 6), IconButton( tooltip: 'Tab history', onPressed: () => context.go('/history'), icon: const Icon(Icons.history_rounded), ), const SizedBox(width: 6), IconButton( tooltip: 'Refresh', onPressed: () => viewModel.load(), icon: const Icon(Icons.refresh_rounded), ), const SizedBox(width: 6), IconButton( tooltip: 'Settings', onPressed: () => context.go('/settings'), icon: const Icon(Icons.settings), ), if (context.watch().isPinSet) ...[ const SizedBox(width: 6), IconButton( tooltip: 'Logout', onPressed: () { Provider.of(context, listen: false).lock(); }, icon: const Icon(Icons.logout), ), ], ], actionsPadding: const EdgeInsets.symmetric(horizontal: 8), ), resizeToAvoidBottomInset: false, body: Builder( builder: (context) { if (viewModel.isLoading) { return const Center( child: CircularProgressIndicator(strokeWidth: 2.5), ); } if (viewModel.errorMessage != null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline_rounded, size: 40, color: Theme.of(context).colorScheme.error, ), const SizedBox(height: 12), Text( viewModel.errorMessage!, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ); } return Row( children: [ Expanded( flex: 2, child: _ProductGrid( products: productsViewModel.products, hasSelectedTab: viewModel.selectedTab != null, onProductTap: (product) async { if (viewModel.selectedTab == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Open or select a tab first.'), ), ); return; } try { await viewModel.addProductToSelectedTab(product); } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('$e')), ); } }, ), ), Container(width: 1, color: Theme.of(context).dividerColor), Expanded( flex: 1, child: _TabPanel( tabs: viewModel.tabs, selectedTab: viewModel.selectedTab, selectedTabId: viewModel.selectedTabId, onNewTabPressed: () => showNewTabDialog(context), onTabSelected: viewModel.selectTab, onItemQuantityChanged: viewModel.changeItemQuantity, onCloseTabPressed: () => confirmCloseTab(context), onTabClosed: viewModel.closeTab, ), ), ], ); }, ), ); } } // --------------------------------------------------------------------------- // Product grid // --------------------------------------------------------------------------- class _ProductGrid extends StatelessWidget { final List products; final bool hasSelectedTab; final ValueChanged onProductTap; const _ProductGrid({ required this.products, required this.hasSelectedTab, required this.onProductTap, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; if (products.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( shape: BoxShape.circle, color: scheme.onSurface.withValues(alpha: 0.05), ), child: Icon( Icons.inventory_2_outlined, size: 40, color: scheme.onSurface.withValues(alpha: 0.3), ), ), const SizedBox(height: 16), Text( 'No products yet', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( 'Add your first product to start selling.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 20), FilledButton.icon( onPressed: () => context.go('/products/new'), icon: const Icon(Icons.add), label: const Text('Add product'), ), ], ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 18, 20, 4), child: Row( children: [ Text('Products', style: Theme.of(context).textTheme.titleLarge), const SizedBox(width: 10), if (!hasSelectedTab) Flexible( child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: scheme.onSurface.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.info_outline_rounded, size: 14, color: scheme.onSurface.withValues(alpha: 0.5), ), const SizedBox(width: 4), Flexible( child: Text( 'Select a tab to add items', overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), ), ], ), ), Expanded( child: LayoutBuilder( builder: (context, constraints) { final columns = ((constraints.maxWidth / 170).floor()) .clamp(3, 5) .toInt(); return GridView.builder( padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), itemCount: products.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: columns, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 1, ), itemBuilder: (context, index) { final product = products[index]; return ProductTile( product: product, enabled: hasSelectedTab, onTap: () => onProductTap(product), ); }, ); }, ), ), ], ); } } // --------------------------------------------------------------------------- // Tab panel // --------------------------------------------------------------------------- class _TabPanel extends StatefulWidget { final List tabs; final BarTab? selectedTab; final String? selectedTabId; final VoidCallback onNewTabPressed; final ValueChanged onTabSelected; final Future Function(TabItem item, int quantity) onItemQuantityChanged; final VoidCallback onCloseTabPressed; final Future Function(String) onTabClosed; const _TabPanel({ required this.tabs, required this.selectedTab, required this.selectedTabId, required this.onNewTabPressed, required this.onTabSelected, required this.onItemQuantityChanged, required this.onCloseTabPressed, required this.onTabClosed, }); @override State<_TabPanel> createState() => _TabPanelState(); } class _TabPanelState extends State<_TabPanel> { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final filteredTabs = widget.tabs.where((tab) { return tab.customerName.toLowerCase().contains( _searchQuery.toLowerCase(), ); }).toList(); return DecoratedBox( // A step darker/lighter than the main surface, whichever direction // the active theme goes — matches how it read against the dark // surface color originally. decoration: BoxDecoration(color: scheme.surfaceContainerLow), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: TextField( controller: _searchController, onChanged: (value) { setState(() { _searchQuery = value; }); }, decoration: const InputDecoration( hintText: 'Search by name…', prefixIcon: Icon(Icons.search_rounded, size: 20), isDense: true, ), ), ), const SizedBox(width: 10), IconButton.filled( onPressed: widget.onNewTabPressed, icon: const Icon(Icons.add_rounded), tooltip: 'Open new tab', ), ], ), const SizedBox(height: 18), Row( children: [ Text( 'OPEN TABS', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w700, letterSpacing: 0.8, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: scheme.onSurface.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(999), ), child: Text( '${filteredTabs.length}', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w700, ), ), ), ], ), const SizedBox(height: 8), SizedBox( height: 190, child: _OpenTabsList( tabs: filteredTabs, selectedTabId: widget.selectedTabId, onTabSelected: widget.onTabSelected, onTabClosed: widget.onTabClosed, ), ), const Divider(height: 28), Expanded( child: widget.selectedTab == null ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.receipt_long_outlined, size: 36, color: scheme.onSurface.withValues(alpha: 0.25), ), const SizedBox(height: 10), Text( 'Select or open a tab', style: Theme.of(context).textTheme.bodyMedium, ), ], ), ) : _SelectedTabDetails( tab: widget.selectedTab!, onItemQuantityChanged: widget.onItemQuantityChanged, onCloseTabPressed: widget.onCloseTabPressed, ), ), ], ), ), ); } } class _OpenTabsList extends StatelessWidget { final List tabs; final String? selectedTabId; final ValueChanged onTabSelected; final Future Function(String) onTabClosed; const _OpenTabsList({ required this.tabs, required this.selectedTabId, required this.onTabSelected, required this.onTabClosed, }); @override Widget build(BuildContext context) { if (tabs.isEmpty) { return Center( child: Text( 'No open tabs', style: Theme.of(context).textTheme.bodyMedium, ), ); } return ListView.separated( itemCount: tabs.length, separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (context, index) { final tab = tabs[index]; final selected = tab.id == selectedTabId; final scheme = Theme.of(context).colorScheme; final primary = scheme.primary; return ClipRRect( borderRadius: BorderRadius.circular(14), child: Slidable( key: ValueKey(tab.id), endActionPane: ActionPane( motion: const DrawerMotion(), extentRatio: 0.6, children: [ SlidableAction( onPressed: (_) => onTabSelected(tab.id), icon: Icons.edit_outlined, label: 'Edit', backgroundColor: Theme.of(context).colorScheme.secondary, foregroundColor: Theme.of(context).colorScheme.onSecondary, ), SlidableAction( onPressed: (_) async { try { await onTabClosed(tab.id); } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Could not close tab: $e')), ); } } }, icon: Icons.close_rounded, label: 'Close', backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), ], ), child: AnimatedContainer( duration: const Duration(milliseconds: 150), decoration: BoxDecoration( color: selected ? primary.withValues(alpha: 0.14) : scheme.onSurface.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(14), border: Border.all( color: selected ? primary.withValues(alpha: 0.6) : scheme.onSurface.withValues(alpha: 0.06), width: selected ? 1.4 : 1, ), ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: () => onTabSelected(tab.id), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), child: SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( tab.customerName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: scheme.onSurface, ), ), const SizedBox(height: 4), Text( '${tab.itemCount} items - ${tab.formattedTotal}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ), ), ), ), ); }, ); } } class _SelectedTabDetails extends StatelessWidget { final BarTab tab; final Future Function(TabItem item, int quantity) onItemQuantityChanged; final VoidCallback onCloseTabPressed; const _SelectedTabDetails({ required this.tab, required this.onItemQuantityChanged, required this.onCloseTabPressed, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( tab.customerName, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleLarge, ), ), Divider(), ], ), const SizedBox(height: 16), Expanded( child: tab.items.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.local_bar_outlined, size: 32, color: scheme.onSurface.withValues(alpha: 0.25), ), const SizedBox(height: 8), Text( 'Tap products to add them', style: Theme.of(context).textTheme.bodyMedium, ), ], ), ) : ListView.separated( itemCount: tab.items.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final item = tab.items[index]; return _TabItemRow( item: item, onQuantityChanged: onItemQuantityChanged, ); }, ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(16), border: Border.all( color: Theme.of( context, ).colorScheme.primary.withValues(alpha: 0.3), ), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Total', style: Theme.of(context).textTheme.bodySmall), Text( tab.formattedTotal, style: Theme.of( context, ).textTheme.headlineMedium?.copyWith(fontSize: 24), ), ], ), ), FilledButton( onPressed: tab.items.isEmpty ? null : onCloseTabPressed, child: const Text('Close tab'), ), ], ), ), ], ); } } class _TabItemRow extends StatelessWidget { final TabItem item; final Future Function(TabItem item, int quantity) onQuantityChanged; const _TabItemRow({required this.item, required this.onQuantityChanged}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.productName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 2), Text( '${item.quantity} × ${item.formattedUnitPrice}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), Container( decoration: BoxDecoration( color: scheme.onSurface.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( visualDensity: VisualDensity.compact, onPressed: () async { try { await onQuantityChanged(item, item.quantity - 1); } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Could not update quantity: $e'), ), ); } } }, icon: const Icon(Icons.remove_rounded, size: 18), ), SizedBox( width: 22, child: Text( '${item.quantity}', textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.w700), ), ), IconButton( visualDensity: VisualDensity.compact, onPressed: () async { try { await onQuantityChanged(item, item.quantity + 1); } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Could not update quantity: $e'), ), ); } } }, icon: const Icon(Icons.add_rounded, size: 18), ), ], ), ), SizedBox( width: 72, child: Text( item.formattedLineTotal, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.w700), ), ), ], ), ); } }