import "package:flutter/material.dart"; import "package:provider/provider.dart"; import "../app/app_router.dart"; import "../app/app_state.dart"; import "../models/cart.dart"; import "../models/menu_item.dart"; import "../services/api.dart"; class MenuBrowseScreen extends StatefulWidget { const MenuBrowseScreen({super.key}); @override State createState() => _MenuBrowseScreenState(); } class _MenuBrowseScreenState extends State { Future>? _future; int? _businessId; int? _servicePointId; int? _userId; List _allItems = []; final Map> _itemsByCategory = {}; final Map> _itemsByParent = {}; int? _asIntNullable(dynamic v) { if (v == null) return null; if (v is int) return v; if (v is num) return v.toInt(); if (v is String) { final s = v.trim(); if (s.isEmpty) return null; return int.tryParse(s); } return null; } @override void didChangeDependencies() { super.didChangeDependencies(); final appState = context.watch(); final u = appState.userId; final args = ModalRoute.of(context)?.settings.arguments; if (args is Map) { final b = _asIntNullable(args["BusinessID"]) ?? _asIntNullable(args["businessId"]); final sp = _asIntNullable(args["ServicePointID"]) ?? _asIntNullable(args["servicePointId"]); if (_businessId != b || _servicePointId != sp || _userId != u || _future == null) { _businessId = b; _servicePointId = sp; _userId = u; if (_businessId != null && _businessId! > 0) { _future = _loadMenu(); } else { _future = Future.value([]); } } } } Future> _loadMenu() async { final items = await Api.listMenuItems(businessId: _businessId!); setState(() { _allItems = items; _organizeItems(); }); return items; } void _organizeItems() { _itemsByCategory.clear(); _itemsByParent.clear(); for (final item in _allItems) { // Skip inactive items if (!item.isActive) continue; if (item.isRootItem) { _itemsByCategory.putIfAbsent(item.categoryId, () => []).add(item); } else { // Prevent an item from being its own child if (item.itemId != item.parentItemId) { _itemsByParent.putIfAbsent(item.parentItemId, () => []).add(item); } } } // Sort items within each category by sortOrder for (final list in _itemsByCategory.values) { list.sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); } // Sort modifiers within each parent for (final list in _itemsByParent.values) { list.sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); } } List _getUniqueCategoryIds() { final categoryIds = _itemsByCategory.keys.toList(); categoryIds.sort(); return categoryIds; } @override Widget build(BuildContext context) { final appState = context.watch(); final businessName = appState.selectedBusinessName ?? "Menu"; return Scaffold( appBar: AppBar( title: Row( children: [ // Business logo if (_businessId != null) Padding( padding: const EdgeInsets.only(right: 12), child: ClipRRect( borderRadius: BorderRadius.circular(6), child: SizedBox( width: 36, height: 36, child: Image.network( "$_imageBaseUrl/logos/$_businessId.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Image.network( "$_imageBaseUrl/logos/$_businessId.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(6), ), child: Icon( Icons.store, size: 20, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ); }, ); }, ), ), ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( businessName, style: const TextStyle(fontSize: 18), ), if (appState.selectedServicePointName != null) Text( appState.selectedServicePointName!, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.normal, ), ), ], ), ), ], ), actions: [ IconButton( icon: Badge( label: Text("${appState.cartItemCount}"), isLabelVisible: appState.cartItemCount > 0, child: const Icon(Icons.shopping_cart), ), onPressed: () { Navigator.of(context).pushNamed(AppRoutes.cartView); }, ), ], ), body: (_businessId == null || _businessId! <= 0) ? const Padding( padding: EdgeInsets.all(16), child: Text("Missing BusinessID"), ) : FutureBuilder>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Text("Error loading menu: ${snapshot.error}"), const SizedBox(height: 16), ElevatedButton( onPressed: () => setState(() => _future = _loadMenu()), child: const Text("Retry"), ), ], ), ); } if (_allItems.isEmpty) { return const Padding( padding: EdgeInsets.all(16), child: Text("No menu items available"), ); } final categoryIds = _getUniqueCategoryIds(); return ListView.builder( itemCount: categoryIds.length + 1, // +1 for header itemBuilder: (context, index) { // First item is the business header if (index == 0) { return _buildBusinessHeader(); } final categoryIndex = index - 1; final categoryId = categoryIds[categoryIndex]; final items = _itemsByCategory[categoryId] ?? []; final categoryName = items.isNotEmpty ? items.first.categoryName : "Category $categoryId"; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCategoryHeader(categoryId, categoryName), ...items.map((item) => _buildMenuItem(item)), const SizedBox(height: 16), ], ); }, ); }, ), ); } static const String _imageBaseUrl = "https://biz.payfrit.com/uploads"; Widget _buildBusinessHeader() { if (_businessId == null) return const SizedBox.shrink(); final appState = context.read(); final businessName = appState.selectedBusinessName ?? "Restaurant"; return Container( width: double.infinity, height: 180, margin: const EdgeInsets.only(bottom: 8), child: Stack( fit: StackFit.expand, children: [ // Header background image Image.network( "$_imageBaseUrl/headers/$_businessId.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Image.network( "$_imageBaseUrl/headers/$_businessId.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { // No header image - show gradient background return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.secondary, ], ), ), ); }, ); }, ), // Dark gradient overlay Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withAlpha(0), Colors.black.withAlpha(179), ], ), ), ), // Business info overlay Positioned( left: 16, right: 16, bottom: 16, child: Row( children: [ // Logo ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( width: 56, height: 56, child: Image.network( "$_imageBaseUrl/logos/$_businessId.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Image.network( "$_imageBaseUrl/logos/$_businessId.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( color: Colors.white24, child: const Icon( Icons.store, size: 32, color: Colors.white, ), ); }, ); }, ), ), ), const SizedBox(width: 12), // Business name and info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( businessName, style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, shadows: [ const Shadow( offset: Offset(1, 1), blurRadius: 3, color: Colors.black54, ), ], ), ), if (appState.selectedServicePointName != null) Text( appState.selectedServicePointName!, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white70, ), ), ], ), ), ], ), ), ], ), ); } Widget _buildItemImage(int itemId) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( width: 64, height: 64, child: Image.network( "$_imageBaseUrl/items/$itemId.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { // Try jpg if png fails return Image.network( "$_imageBaseUrl/items/$itemId.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { // Show placeholder if no image return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.restaurant, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 28, ), ); }, ); }, ), ), ); } Widget _buildCategoryHeader(int categoryId, String categoryName) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, ), child: Stack( fit: StackFit.expand, children: [ // Category image background Image.network( "$_imageBaseUrl/categories/$categoryId.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Image.network( "$_imageBaseUrl/categories/$categoryId.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { // No image - just show solid color return Container( color: Theme.of(context).colorScheme.primaryContainer, ); }, ); }, ), // Dark gradient overlay for text readability Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withAlpha(0), Colors.black.withAlpha(179), ], ), ), ), // Category name Positioned( left: 16, bottom: 12, right: 16, child: Text( categoryName, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: Colors.white, shadows: [ const Shadow( offset: Offset(1, 1), blurRadius: 3, color: Colors.black54, ), ], ), ), ), ], ), ); } Widget _buildMenuItem(MenuItem item) { final hasModifiers = _itemsByParent.containsKey(item.itemId); return Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( color: Theme.of(context).colorScheme.outlineVariant.withAlpha(128), ), ), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () { if (hasModifiers) { _showItemCustomization(item); } else { _addToCart(item, {}); } }, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ // Item image or placeholder _buildItemImage(item.itemId), const SizedBox(width: 16), // Item details Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.name, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), if (item.description.isNotEmpty) ...[ const SizedBox(height: 4), Text( item.description, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], if (hasModifiers) ...[ const SizedBox(height: 4), Text( "Customizable", style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.w500, ), ), ], ], ), ), const SizedBox(width: 12), // Price and add button Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( "\$${item.price.toStringAsFixed(2)}", style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 4), Icon( Icons.add_circle, color: Theme.of(context).colorScheme.primary, size: 20, ), ], ), ], ), ), ), ); } void _showItemCustomization(MenuItem item) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => _ItemCustomizationSheet( item: item, itemsByParent: _itemsByParent, onAdd: (selectedItemIds) { Navigator.pop(context); _addToCart(item, selectedItemIds); }, ), ); } Future _addToCart(MenuItem item, Set selectedModifierIds) async { // Check if user is logged in - if not, navigate to login if (_userId == null) { final shouldLogin = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Login Required"), content: const Text("Please login to add items to your cart."), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text("Cancel"), ), FilledButton( onPressed: () => Navigator.pop(context, true), child: const Text("Login"), ), ], ), ); if (shouldLogin == true && mounted) { Navigator.of(context).pushNamed(AppRoutes.login); } return; } if (_businessId == null || _servicePointId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Missing required information")), ); return; } try { final appState = context.read(); // Get or create cart Cart cart; if (appState.cartOrderId == null) { cart = await Api.getOrCreateCart( userId: _userId!, businessId: _businessId!, servicePointId: _servicePointId!, orderTypeId: 1, // Dine-in ); appState.setCartOrder( orderId: cart.orderId, orderUuid: cart.orderUuid, itemCount: cart.itemCount, ); } else { // We have an existing cart ID cart = await Api.getCart(orderId: appState.cartOrderId!); } // Check if this item already exists in the cart (as a root item) final existingItem = cart.lineItems.where( (li) => li.itemId == item.itemId && li.parentOrderLineItemId == 0 && !li.isDeleted ).firstOrNull; final newQuantity = (existingItem?.quantity ?? 0) + 1; // Add root item (or update quantity if it exists) cart = await Api.setLineItem( orderId: cart.orderId, parentOrderLineItemId: 0, itemId: item.itemId, isSelected: true, quantity: newQuantity, ); // Find the OrderLineItemID of the root item we just added final rootLineItem = cart.lineItems.lastWhere( (li) => li.itemId == item.itemId && li.parentOrderLineItemId == 0 && !li.isDeleted, orElse: () => throw StateError('Root line item not found for ItemID=${item.itemId}'), ); // Add all selected modifiers recursively print('[MenuBrowse] Adding ${selectedModifierIds.length} modifiers to root item OrderLineItemID=${rootLineItem.orderLineItemId}'); await _addModifiersRecursively( cart.orderId, rootLineItem.orderLineItemId, item.itemId, selectedModifierIds, ); // Refresh cart to get final state print('[MenuBrowse] Refreshing cart to get final state'); cart = await Api.getCart(orderId: cart.orderId); print('[MenuBrowse] Final cart has ${cart.lineItems.length} total line items'); appState.updateCartItemCount(cart.itemCount); if (!mounted) return; final message = selectedModifierIds.isEmpty ? "Added ${item.name} to cart (${cart.itemCount} item${cart.itemCount == 1 ? '' : 's'})" : "Added ${item.name} with ${selectedModifierIds.length} customizations (${cart.itemCount} item${cart.itemCount == 1 ? '' : 's'})"; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message)), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error adding to cart: $e")), ); } } Future _addModifiersRecursively( int orderId, int parentOrderLineItemId, int parentItemId, Set selectedItemIds, ) async { final children = _itemsByParent[parentItemId] ?? []; print('[MenuBrowse] _addModifiersRecursively: parentItemId=$parentItemId has ${children.length} children'); for (final child in children) { final isSelected = selectedItemIds.contains(child.itemId); final grandchildren = _itemsByParent[child.itemId] ?? []; final hasGrandchildren = grandchildren.isNotEmpty; final hasSelectedDescendants = _hasSelectedDescendants(child.itemId, selectedItemIds); print('[MenuBrowse] Child ${child.name} (ItemID=${child.itemId}): selected=$isSelected, hasChildren=$hasGrandchildren, hasSelectedDescendants=$hasSelectedDescendants'); // Add this item if it's selected OR if it has selected descendants (to maintain hierarchy) if (isSelected || hasSelectedDescendants) { print('[MenuBrowse] Adding ${isSelected ? "selected" : "container"} item ${child.name} with ParentOrderLineItemID=$parentOrderLineItemId'); final cart = await Api.setLineItem( orderId: orderId, parentOrderLineItemId: parentOrderLineItemId, itemId: child.itemId, isSelected: true, ); // Find the OrderLineItemID of this item we just added final childLineItem = cart.lineItems.lastWhere( (li) => li.itemId == child.itemId && li.parentOrderLineItemId == parentOrderLineItemId && !li.isDeleted, orElse: () => throw StateError('Child line item not found for ItemID=${child.itemId}'), ); // Recursively add children with this item as the new parent if (hasGrandchildren) { await _addModifiersRecursively( orderId, childLineItem.orderLineItemId, child.itemId, selectedItemIds, ); } } } } /// Check if any descendants of this item are selected bool _hasSelectedDescendants(int itemId, Set selectedItemIds) { final children = _itemsByParent[itemId] ?? []; for (final child in children) { if (selectedItemIds.contains(child.itemId)) { return true; } if (_hasSelectedDescendants(child.itemId, selectedItemIds)) { return true; } } return false; } } /// Recursive item customization sheet with full rule support class _ItemCustomizationSheet extends StatefulWidget { final MenuItem item; final Map> itemsByParent; final Function(Set) onAdd; const _ItemCustomizationSheet({ required this.item, required this.itemsByParent, required this.onAdd, }); @override State<_ItemCustomizationSheet> createState() => _ItemCustomizationSheetState(); } class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> { final Set _selectedItemIds = {}; String? _validationError; @override void initState() { super.initState(); _initializeDefaults(widget.item.itemId); } /// Recursively initialize default selections void _initializeDefaults(int parentId) { final children = widget.itemsByParent[parentId] ?? []; for (final child in children) { if (child.isCheckedByDefault) { _selectedItemIds.add(child.itemId); _initializeDefaults(child.itemId); } } } /// Calculate total price including all selected items recursively double _calculateTotal() { double total = widget.item.price; void addPriceRecursively(int itemId) { final children = widget.itemsByParent[itemId] ?? []; for (final child in children) { if (_selectedItemIds.contains(child.itemId)) { total += child.price; addPriceRecursively(child.itemId); } } } addPriceRecursively(widget.item.itemId); return total; } /// Validate selections before adding to cart bool _validate() { setState(() => _validationError = null); bool validateRecursive(int parentId, MenuItem parent) { final children = widget.itemsByParent[parentId] ?? []; if (children.isEmpty) return true; final selectedChildren = children.where((c) => _selectedItemIds.contains(c.itemId)).toList(); // Check if child selection is required if (parent.requiresChildSelection && selectedChildren.isEmpty) { setState(() => _validationError = "Please select an option for: ${parent.name}"); return false; } // Check max selection limit if (parent.maxNumSelectionReq > 0 && selectedChildren.length > parent.maxNumSelectionReq) { setState(() => _validationError = "${parent.name}: Max ${parent.maxNumSelectionReq} selections allowed"); return false; } // Recursively validate selected children for (final child in selectedChildren) { if (!validateRecursive(child.itemId, child)) { return false; } } return true; } return validateRecursive(widget.item.itemId, widget.item); } void _handleAdd() { if (_validate()) { widget.onAdd(_selectedItemIds); } } @override Widget build(BuildContext context) { return DraggableScrollableSheet( initialChildSize: 0.75, minChildSize: 0.5, maxChildSize: 0.95, expand: false, builder: (context, scrollController) { return Column( children: [ // Header Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border( bottom: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, width: 1, ), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 12, height: 4, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.onSurfaceVariant, borderRadius: BorderRadius.circular(2), ), ), ], ), const SizedBox(height: 16), Text( widget.item.name, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), if (widget.item.description.isNotEmpty) ...[ const SizedBox(height: 8), Text( widget.item.description, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(20), ), child: Text( "Base: \$${widget.item.price.toStringAsFixed(2)}", style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.onPrimaryContainer, fontWeight: FontWeight.w600, ), ), ), ], ), ), // Scrollable content Expanded( child: ListView( controller: scrollController, padding: const EdgeInsets.all(16), children: _buildModifierTree(widget.item.itemId, 0), ), ), // Footer with validation error and add button Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( color: Colors.black.withAlpha(25), blurRadius: 4, offset: const Offset(0, -2), ), ], ), child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (_validationError != null) ...[ Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade300), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red.shade700), const SizedBox(width: 12), Expanded( child: Text( _validationError!, style: TextStyle(color: Colors.red.shade900), ), ), ], ), ), ], SizedBox( width: double.infinity, child: FilledButton( onPressed: _handleAdd, child: Text("Add to Cart - \$${_calculateTotal().toStringAsFixed(2)}"), ), ), ], ), ), ), ], ); }, ); } /// Recursively build modifier tree List _buildModifierTree(int parentId, int depth) { final children = widget.itemsByParent[parentId] ?? []; if (children.isEmpty) return []; final parent = _findItemById(parentId); if (parent == null) return []; final widgets = []; for (final child in children) { final hasGrandchildren = widget.itemsByParent.containsKey(child.itemId); if (hasGrandchildren && child.isCollapsible) { // Collapsible section with ExpansionTile widgets.add(_buildExpansionTile(child, parent, depth)); } else { // Regular checkbox/radio item widgets.add(_buildSelectableItem(child, parent, depth)); // Recursively add grandchildren if (hasGrandchildren && _selectedItemIds.contains(child.itemId)) { widgets.addAll(_buildModifierTree(child.itemId, depth + 1)); } } } return widgets; } Widget _buildExpansionTile(MenuItem item, MenuItem parent, int depth) { final isSelected = _selectedItemIds.contains(item.itemId); return Padding( padding: EdgeInsets.only(left: depth * 16.0), child: ExpansionTile( title: Text(item.name), subtitle: item.price > 0 ? Text("+\$${item.price.toStringAsFixed(2)}") : null, initiallyExpanded: isSelected || item.isCheckedByDefault, leading: _buildSelectionWidget(item, parent), children: _buildModifierTree(item.itemId, depth + 1), ), ); } Widget _buildSelectableItem(MenuItem item, MenuItem parent, int depth) { return Padding( padding: EdgeInsets.only(left: depth * 16.0), child: ListTile( leading: _buildSelectionWidget(item, parent), title: Text(item.name), subtitle: item.price > 0 ? Text("+\$${item.price.toStringAsFixed(2)}") : null, onTap: () => _toggleSelection(item, parent), ), ); } Widget _buildSelectionWidget(MenuItem item, MenuItem parent) { final isSelected = _selectedItemIds.contains(item.itemId); // Radio button if max selection is 1 if (parent.maxNumSelectionReq == 1) { return Radio( value: item.itemId, groupValue: _getSelectedInGroup(parent.itemId), onChanged: (_) => _toggleSelection(item, parent), ); } // Checkbox for multiple or unlimited selections return Checkbox( value: isSelected, onChanged: (_) => _toggleSelection(item, parent), ); } int? _getSelectedInGroup(int parentId) { final children = widget.itemsByParent[parentId] ?? []; for (final child in children) { if (_selectedItemIds.contains(child.itemId)) { return child.itemId; } } return null; } void _toggleSelection(MenuItem item, MenuItem parent) { setState(() { _validationError = null; final isCurrentlySelected = _selectedItemIds.contains(item.itemId); // For radio buttons (max = 1), deselect siblings and always select the clicked item if (parent.maxNumSelectionReq == 1) { final siblings = widget.itemsByParent[parent.itemId] ?? []; for (final sibling in siblings) { _selectedItemIds.remove(sibling.itemId); _deselectDescendants(sibling.itemId); } // Always select the clicked item (radio buttons can't be deselected) _selectedItemIds.add(item.itemId); return; } // For checkboxes, allow toggle on/off if (isCurrentlySelected) { // Deselect this item and all descendants _selectedItemIds.remove(item.itemId); _deselectDescendants(item.itemId); } else { // Check max selection limit if (parent.maxNumSelectionReq > 0) { final siblings = widget.itemsByParent[parent.itemId] ?? []; final selectedSiblings = siblings.where((s) => _selectedItemIds.contains(s.itemId)).length; if (selectedSiblings >= parent.maxNumSelectionReq) { _validationError = "${parent.name}: Max ${parent.maxNumSelectionReq} selections allowed"; return; } } // Select this item _selectedItemIds.add(item.itemId); } }); } void _deselectDescendants(int parentId) { final children = widget.itemsByParent[parentId] ?? []; for (final child in children) { _selectedItemIds.remove(child.itemId); _deselectDescendants(child.itemId); } } MenuItem? _findItemById(int itemId) { for (final list in widget.itemsByParent.values) { for (final item in list) { if (item.itemId == itemId) return item; } } return widget.item.itemId == itemId ? widget.item : null; } }