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 '../services/product_service.dart'; import '../utils/app_updater.dart'; import '../viewmodels/bar_screen_view_model.dart'; import '../viewmodels/settings_view_model.dart'; import '../l10n/app_localizations.dart'; import '../l10n/app_localizations_helpers.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 l10n = AppLocalizations.of(context); final viewModel = context.watch(); final productsViewModel = context.watch(); final settings = context.watch().settings; final stockByProductId = { for (final product in productsViewModel.products) product.id: product.stockQuantity, }; return Scaffold( appBar: AppBar( title: Text(l10n.barTabs), actions: [ IconButton( tooltip: l10n.manageProducts, onPressed: () => context.go('/products'), icon: const Icon(Icons.inventory_2_outlined), ), const SizedBox(width: 6), IconButton( tooltip: l10n.tabHistory, onPressed: () => context.go('/history'), icon: const Icon(Icons.history_rounded), ), const SizedBox(width: 6), IconButton( tooltip: l10n.refresh, onPressed: () => viewModel.load(), icon: const Icon(Icons.refresh_rounded), ), const SizedBox(width: 6), IconButton( tooltip: l10n.settings, onPressed: () => context.go('/settings'), icon: const Icon(Icons.settings), ), if (context.watch().isPinSet) ...[ const SizedBox(width: 6), IconButton( tooltip: l10n.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( l10n.localizedError(viewModel.errorMessage), textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ); } return Row( children: [ Expanded( flex: 2, child: _ProductGrid( products: productsViewModel.products, rows: settings.barGridRows, hasSelectedTab: viewModel.selectedTab != null, onProductTap: (product) async { if (viewModel.selectedTab == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.openOrSelectTab)), ); return; } try { await viewModel.addProductToSelectedTab(product); } catch (e, stack) { if (e is! InsufficientStockException) { Sentry.captureException(e, stackTrace: stack); } if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( e is InsufficientStockException ? l10n.productOutOfStock : l10n.couldNotAddProductToTab, ), ), ); } }, ), ), Container(width: 1, color: Theme.of(context).dividerColor), Expanded( flex: 1, child: _TabPanel( tabs: viewModel.tabs, selectedTab: viewModel.selectedTab, selectedTabId: viewModel.selectedTabId, stockByProductId: stockByProductId, 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 int rows; final bool hasSelectedTab; final ValueChanged onProductTap; const _ProductGrid({ required this.products, required this.rows, required this.hasSelectedTab, required this.onProductTap, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context); 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( l10n.noProductsYet, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( l10n.addFirstProduct, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 20), FilledButton.icon( onPressed: () => context.go('/products/new'), icon: const Icon(Icons.add), label: Text(l10n.addProduct), ), ], ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 18, 20, 4), child: Row( children: [ Text( l10n.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( l10n.selectTabToAddItems, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), ), ], ), ), Expanded( child: LayoutBuilder( builder: (context, constraints) { const horizontalPadding = 32.0; const verticalPadding = 28.0; const spacing = 12.0; final rowCount = rows.clamp(2, 6).toInt(); final rowHeight = ((constraints.maxHeight - verticalPadding - (rowCount - 1) * spacing) / rowCount) .clamp(96.0, 320.0) .toDouble(); final columns = (((constraints.maxWidth - horizontalPadding + spacing) / (rowHeight + spacing)) .ceil()) .clamp(2, 8) .toInt(); return GridView.builder( key: ValueKey('product-grid-$rows'), 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 Map stockByProductId; 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.stockByProductId, 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 l10n = AppLocalizations.of(context); 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: InputDecoration( hintText: l10n.searchByName, 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: l10n.openNewTab, ), ], ), const SizedBox(height: 18), Row( children: [ Text( l10n.openTabs.toUpperCase(), 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( l10n.selectOrOpenTab, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ) : _SelectedTabDetails( tab: widget.selectedTab!, stockByProductId: widget.stockByProductId, 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) { final l10n = AppLocalizations.of(context); if (tabs.isEmpty) { return Center( child: Text( l10n.noOpenTabs, 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: l10n.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(l10n.couldNotCloseTab)), ); } } }, icon: Icons.close_rounded, label: l10n.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( l10n.tabItemSummary( tab.itemCount, tab.formattedTotal, ), style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ), ), ), ), ); }, ); } } class _SelectedTabDetails extends StatelessWidget { final BarTab tab; final Map stockByProductId; final Future Function(TabItem item, int quantity) onItemQuantityChanged; final VoidCallback onCloseTabPressed; const _SelectedTabDetails({ required this.tab, required this.stockByProductId, required this.onItemQuantityChanged, required this.onCloseTabPressed, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context); 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( l10n.tapProductsToAdd, 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, stockByProductId: stockByProductId, 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( l10n.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: Text(l10n.closeTab), ), ], ), ), ], ); } } class _TabItemRow extends StatelessWidget { final TabItem item; final Map stockByProductId; final Future Function(TabItem item, int quantity) onQuantityChanged; const _TabItemRow({ required this.item, required this.stockByProductId, required this.onQuantityChanged, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context); final availableStock = stockByProductId[item.productId]; final canIncrease = availableStock == null || availableStock > 0; 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(l10n.couldNotUpdateQuantity)), ); } } }, 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: canIncrease ? () async { try { await onQuantityChanged(item, item.quantity + 1); } catch (e, stack) { if (e is! InsufficientStockException) { Sentry.captureException(e, stackTrace: stack); } if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( e is InsufficientStockException ? l10n.productOutOfStock : l10n.couldNotUpdateQuantity, ), ), ); } } } : null, icon: const Icon(Icons.add_rounded, size: 18), ), ], ), ), SizedBox( width: 72, child: Text( item.formattedLineTotal, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.w700), ), ), ], ), ); } }