import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widget_previews.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:provider/provider.dart'; import 'dart:io'; import 'package:flutter_slidable/flutter_slidable.dart'; import '../models/bar_tab.dart'; import '../models/product.dart'; import '../models/tab_item.dart'; import '../viewmodels/bar_screen_view_model.dart'; class BarScreenView extends StatelessWidget { const BarScreenView({super.key}); @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: 'logout', onPressed: (){ Provider.of(context, listen: false).lock(); }, icon: const Icon(Icons.logout) ), const SizedBox(width: 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; } await viewModel.addProductToSelectedTab(product); }, ), ), 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, ), ), ], ); }, ), ); } Future _showNewTabDialog(BuildContext context) async { final controller = TextEditingController(); final name = await showDialog( context: context, builder: (dialogContext) { return AlertDialog( title: const Text('Open new tab'), content: TextField( controller: controller, autofocus: true, decoration: const InputDecoration( labelText: 'Customer / group name', ), onSubmitted: (value) { Navigator.of(dialogContext).pop(value); }, ), actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { Navigator.of(dialogContext).pop(controller.text); }, child: const Text('Open tab'), ), ], ); }, ); if (name == null || name.trim().isEmpty) return; if (!context.mounted) return; await context.read().createTab(name.trim()); } Future _confirmCloseTab(BuildContext context) async { final viewModel = context.read(); final tab = viewModel.selectedTab; if (tab == null) return; final confirmed = await showDialog( context: context, builder: (dialogContext) { return AlertDialog( title: Text('Close ${tab.customerName}ʼs tab?'), content: Text('Current total: ${tab.formattedTotal}\n\n'), actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel'), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: Theme.of(dialogContext).colorScheme.error, ), onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('Close tab'), ), ], ); }, ); if (confirmed != true) return; await viewModel.closeSelectedTab(); } } // --------------------------------------------------------------------------- // 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.withOpacity(0.05), ), child: Icon( Icons.inventory_2_outlined, size: 40, color: scheme.onSurface.withOpacity(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.withOpacity(0.06), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.info_outline_rounded, size: 14, color: scheme.onSurface.withOpacity(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), ); }, ); }, ), ), ], ); } } class _ProductTile extends StatelessWidget { final Product product; final bool enabled; final VoidCallback onTap; const _ProductTile({ required this.product, required this.enabled, required this.onTap, }); bool get _hasImage { final imagePath = product.imagePath; if (imagePath == null || imagePath.isEmpty) { return false; } return File(imagePath).existsSync(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all(color: scheme.onSurface.withOpacity(0.08), width: 1), ), child: ClipRRect( borderRadius: BorderRadius.circular(15), child: Material( color: theme.cardTheme.color ?? scheme.surface, child: InkWell( onTap: enabled ? onTap : null, child: Opacity( opacity: enabled ? 1 : 0.4, child: Stack( fit: StackFit.expand, children: [ _hasImage ? Image.file( File(product.imagePath!), fit: BoxFit.scaleDown, width: double.infinity, height: double.infinity, ) : Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.image_not_supported_outlined, size: 34, color: scheme.onSurface.withOpacity(0.3), ), const SizedBox(height: 6), Text( 'No image', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), // Scrim stays black regardless of theme — it's for legibility // of the (usually light/photographic) image beneath it, not // themed UI chrome. if (_hasImage) Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withOpacity(0.35), ], stops: const [0.6, 1.0], ), ), ), ), ], ), ), ), ), ), ); } } // --------------------------------------------------------------------------- // 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 ValueChanged 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.withOpacity(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.withOpacity(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 ValueChanged 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); // TODO: rename/edit tab }, icon: Icons.edit_outlined, label: 'Edit', backgroundColor: Theme.of(context).colorScheme.secondary, foregroundColor: Theme.of(context).colorScheme.onSecondary, ), SlidableAction( onPressed: (_) => onTabClosed(tab.id), 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.withOpacity(0.14) : scheme.onSurface.withOpacity(0.04), borderRadius: BorderRadius.circular(14), border: Border.all( color: selected ? primary.withOpacity(0.6) : scheme.onSurface.withOpacity(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.withOpacity(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.withOpacity(0.12), borderRadius: BorderRadius.circular(16), border: Border.all( color: Theme.of(context).colorScheme.primary.withOpacity(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: 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.withOpacity(0.05), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( visualDensity: VisualDensity.compact, onPressed: () => onQuantityChanged(item, item.quantity - 1), 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: () => onQuantityChanged(item, item.quantity + 1), icon: const Icon(Icons.add_rounded, size: 18), ), ], ), ), SizedBox( width: 72, child: Text( item.formattedLineTotal, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.w700), ), ), ], ), ); } }