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/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( actions: [ IconButton( tooltip: 'Manage products', onPressed: () => context.go('/products'), icon: const Icon(Icons.inventory_2_outlined), ), IconButton( tooltip: 'Refresh', onPressed: viewModel.load, icon: const Icon(Icons.refresh), ), ], ), resizeToAvoidBottomInset: false, body: Builder( builder: (context) { if (viewModel.isLoading) { return const Center(child: CircularProgressIndicator()); } if (viewModel.errorMessage != null) { return Center(child: Text(viewModel.errorMessage!)); } return Row( children: [ Expanded( flex: 3, 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); }, ), ), const VerticalDivider(width: 1), 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', border: OutlineInputBorder(), ), onSubmitted: (value) { Navigator.of(dialogContext).pop(value); }, ), 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' 'Payments are not handled yet, so this only marks the tab as closed.', ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('Close tab'), ), ], ); }, ); if (confirmed != true) return; await viewModel.closeSelectedTab(); } } 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) { if (products.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.inventory_2_outlined, size: 48), const SizedBox(height: 12), const Text('No products yet.'), const SizedBox(height: 12), FilledButton.icon( onPressed: () => context.go('/products/new'), icon: const Icon(Icons.add), label: const Text('Add product'), ), ], ), ); } return LayoutBuilder( builder: (context, constraints) { final columns = ((constraints.maxWidth / 170).floor()) .clamp(3, 6) .toInt(); return GridView.builder( padding: const EdgeInsets.all(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) { return Card( clipBehavior: Clip.antiAlias, margin: EdgeInsets.zero, child: InkWell( onTap: enabled ? onTap : null, child: Opacity( opacity: enabled ? 1 : 0.45, child: _hasImage ? Image.file( File(product.imagePath!), fit: BoxFit.scaleDown, width: double.infinity, height: double.infinity, ) : const Center( child: Stack( children: [ Icon(Icons.image_not_supported_outlined, size: 42), Text("No Image found!"), ], ), ), ), ), ); } } 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 filteredTabs = widget.tabs.where((tab) { return tab.customerName.toLowerCase().contains( _searchQuery.toLowerCase(), ); }).toList(); return Padding( padding: const EdgeInsets.all(12), child: Column( children: [ SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: widget.onNewTabPressed, icon: const Icon(Icons.add), label: const Text('Open tab'), ), ), const SizedBox(height: 12), TextField( controller: _searchController, onChanged: (value) { setState(() { _searchQuery = value; }); }, decoration: const InputDecoration( hintText: 'Search name...', prefixIcon: Icon(Icons.search), ), ), const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, child: Text( 'Open tabs', style: Theme.of(context).textTheme.titleMedium, ), ), const SizedBox(height: 8), SizedBox( height: 200, child: _OpenTabsList( tabs: filteredTabs, selectedTabId: widget.selectedTabId, onTabSelected: widget.onTabSelected, onTabClosed: widget.onTabClosed, ), ), const Divider(height: 24), Expanded( child: widget.selectedTab == null ? const Center(child: Text('Select or open a tab.')) : _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 const Center(child: Text('No open tabs.')); } return ListView.separated( itemCount: tabs.length, separatorBuilder: (_, __) => const SizedBox(height: 6), itemBuilder: (context, index) { final tab = tabs[index]; final selected = tab.id == selectedTabId; return ClipRRect( child: Slidable( key: ValueKey(tab.id), endActionPane: ActionPane( motion: const DrawerMotion(), extentRatio: 0.65, 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, label: 'Close', backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), ], ), child: Card( margin: EdgeInsets.zero, color: selected ? Theme.of(context).colorScheme.primaryContainer : null, child: ListTile( dense: true, selected: selected, title: Text( tab.customerName, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text('${tab.itemCount} items • ${tab.formattedTotal}'), onTap: () => onTabSelected(tab.id), ), ), ), ); }, ); } } 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) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tab.customerName, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 4), Text( 'Total: ${tab.formattedTotal}', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 12), Expanded( child: tab.items.isEmpty ? const Center(child: Text('Tap products to add them.')) : 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 Divider(height: 24), Row( children: [ Expanded( child: Text( tab.formattedTotal, style: Theme.of(context).textTheme.headlineSmall, ), ), FilledButton( onPressed: onCloseTabPressed, child: const Text('Close'), ), ], ), ], ); } } 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) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.productName, maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( '${item.quantity} × ${item.formattedUnitPrice}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), IconButton( visualDensity: VisualDensity.compact, onPressed: () => onQuantityChanged(item, item.quantity - 1), icon: const Icon(Icons.remove_circle_outline), ), Text('${item.quantity}'), IconButton( visualDensity: VisualDensity.compact, onPressed: () => onQuantityChanged(item, item.quantity + 1), icon: const Icon(Icons.add_circle_outline), ), SizedBox( width: 72, child: Text(item.formattedLineTotal, textAlign: TextAlign.end), ), ], ), ); } }