// lib/screens/restaurant_select_screen.dart import "package:flutter/material.dart"; import "package:provider/provider.dart"; import "../app/app_state.dart"; import "../models/menu_item.dart"; import "../models/restaurant.dart"; import "../models/service_point.dart"; import "../services/api.dart"; class RestaurantSelectScreen extends StatefulWidget { const RestaurantSelectScreen({super.key}); @override State createState() => _RestaurantSelectScreenState(); } class _RestaurantSelectScreenState extends State { late Future> _restaurantsFuture; String? _debugLastRaw; int? _debugLastStatus; // Which restaurant is currently expanded (shows menu) int? _expandedBusinessId; // Cache for loaded menus and service points final Map> _menuCache = {}; final Map> _servicePointCache = {}; final Map _loadingMenu = {}; static const String _imageBaseUrl = "https://biz.payfrit.com/uploads"; @override void initState() { super.initState(); _restaurantsFuture = _loadRestaurants(); // Clear order type when arriving at restaurant select (no beacon = not dine-in) // This ensures the table change icon doesn't appear for delivery/takeaway orders WidgetsBinding.instance.addPostFrameCallback((_) { final appState = context.read(); appState.setOrderType(null); }); } Future> _loadRestaurants() async { final raw = await Api.listRestaurantsRaw(); _debugLastRaw = raw.rawBody; _debugLastStatus = raw.statusCode; return Api.listRestaurants(); } Future _loadMenuForBusiness(int businessId) async { if (_menuCache.containsKey(businessId)) return; if (_loadingMenu[businessId] == true) return; setState(() => _loadingMenu[businessId] = true); try { // Load menu items and service points in parallel final results = await Future.wait([ Api.listMenuItems(businessId: businessId), Api.listServicePoints(businessId: businessId), ]); if (mounted) { setState(() { _menuCache[businessId] = results[0] as List; _servicePointCache[businessId] = results[1] as List; _loadingMenu[businessId] = false; }); } } catch (e) { debugPrint('[RestaurantSelect] Error loading menu: $e'); if (mounted) { setState(() => _loadingMenu[businessId] = false); } } } void _toggleExpand(Restaurant restaurant) { // For delivery/takeaway flow (no beacon), go directly to menu // No need to select table - just pick the first service point _navigateToMenu(restaurant); } void _navigateToMenu(Restaurant restaurant) async { // Load service points if not cached if (!_servicePointCache.containsKey(restaurant.businessId)) { try { final servicePoints = await Api.listServicePoints(businessId: restaurant.businessId); _servicePointCache[restaurant.businessId] = servicePoints; } catch (e) { debugPrint('[RestaurantSelect] Error loading service points: $e'); } } if (!mounted) return; // Default to first service point (for delivery/takeaway, table doesn't matter) final servicePoints = _servicePointCache[restaurant.businessId]; final servicePointId = servicePoints?.firstOrNull?.servicePointId ?? 1; // Set app state final appState = context.read(); appState.setBusinessAndServicePoint( restaurant.businessId, servicePointId, businessName: restaurant.name, servicePointName: servicePoints?.firstOrNull?.name, ); Api.setBusinessId(restaurant.businessId); // Navigate to full menu browse screen Navigator.of(context).pushReplacementNamed( '/menu_browse', arguments: { 'businessId': restaurant.businessId, 'servicePointId': servicePointId, }, ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.black, foregroundColor: Colors.white, title: const Text("Nearby Restaurants"), elevation: 0, ), body: FutureBuilder>( future: _restaurantsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator(color: Colors.white), ); } if (snapshot.hasError) { return _ErrorPane( title: "Failed to Load", message: snapshot.error.toString(), statusCode: _debugLastStatus, raw: _debugLastRaw, onRetry: () => setState(() => _restaurantsFuture = _loadRestaurants()), ); } final restaurants = snapshot.data ?? const []; if (restaurants.isEmpty) { return _ErrorPane( title: "No Restaurants Found", message: "No Payfrit restaurants nearby.", statusCode: _debugLastStatus, raw: _debugLastRaw, onRetry: () => setState(() => _restaurantsFuture = _loadRestaurants()), ); } return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: restaurants.length, itemBuilder: (context, index) { final restaurant = restaurants[index]; final isExpanded = _expandedBusinessId == restaurant.businessId; return _RestaurantBar( restaurant: restaurant, isExpanded: isExpanded, onTap: () => _toggleExpand(restaurant), menuItems: _menuCache[restaurant.businessId], servicePoints: _servicePointCache[restaurant.businessId], isLoading: _loadingMenu[restaurant.businessId] == true, imageBaseUrl: _imageBaseUrl, ); }, ); }, ), ); } } class _RestaurantBar extends StatelessWidget { final Restaurant restaurant; final bool isExpanded; final VoidCallback onTap; final List? menuItems; final List? servicePoints; final bool isLoading; final String imageBaseUrl; const _RestaurantBar({ required this.restaurant, required this.isExpanded, required this.onTap, required this.menuItems, required this.servicePoints, required this.isLoading, required this.imageBaseUrl, }); @override Widget build(BuildContext context) { return Column( children: [ // Restaurant header bar with logo GestureDetector( onTap: onTap, child: Container( height: 80, margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: isExpanded ? Theme.of(context).colorScheme.primaryContainer : Colors.grey.shade900, ), child: Stack( children: [ // Background header image (subtle) ClipRRect( borderRadius: BorderRadius.circular(12), child: Opacity( opacity: 0.3, child: SizedBox( width: double.infinity, height: 80, child: Image.network( "$imageBaseUrl/headers/${restaurant.businessId}.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Image.network( "$imageBaseUrl/headers/${restaurant.businessId}.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const SizedBox.shrink(); }, ); }, ), ), ), ), // Sharp gradient edges Positioned( left: 0, top: 0, bottom: 0, width: 20, child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), bottomLeft: Radius.circular(12), ), gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ isExpanded ? Colors.transparent : Colors.grey.shade900, Colors.transparent, ], ), ), ), ), Positioned( right: 0, top: 0, bottom: 0, width: 20, child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only( topRight: Radius.circular(12), bottomRight: Radius.circular(12), ), gradient: LinearGradient( begin: Alignment.centerRight, end: Alignment.centerLeft, colors: [ isExpanded ? Colors.transparent : Colors.grey.shade900, Colors.transparent, ], ), ), ), ), // Content Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ // Logo (56x56 recommended, or 112x112 for 2x) ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( width: 56, height: 56, child: Image.network( "$imageBaseUrl/logos/${restaurant.businessId}.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Image.network( "$imageBaseUrl/logos/${restaurant.businessId}.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { // Text-based fallback with first letter return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.tertiary, ], ), ), alignment: Alignment.center, child: Text( restaurant.name.isNotEmpty ? restaurant.name[0].toUpperCase() : "?", style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, ); }, ), ), ), const SizedBox(width: 16), // Name Expanded( child: Text( restaurant.name, style: TextStyle( color: isExpanded ? Theme.of(context).colorScheme.onPrimaryContainer : Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), ), ), ], ), ), ], ), ), ), // Expanded menu content AnimatedCrossFade( firstChild: const SizedBox.shrink(), secondChild: _buildExpandedContent(context), crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: const Duration(milliseconds: 300), sizeCurve: Curves.easeInOut, ), ], ); } Widget _buildExpandedContent(BuildContext context) { if (isLoading) { return Container( padding: const EdgeInsets.all(32), child: const Center( child: CircularProgressIndicator(color: Colors.white), ), ); } if (menuItems == null || menuItems!.isEmpty) { return Container( padding: const EdgeInsets.all(24), margin: const EdgeInsets.symmetric(horizontal: 12), child: const Center( child: Text( "No menu available", style: TextStyle(color: Colors.white70), ), ), ); } // Organize menu items by category final itemsByCategory = >{}; for (final item in menuItems!) { if (item.isRootItem && item.isActive) { itemsByCategory.putIfAbsent(item.categoryId, () => []).add(item); } } // Sort within categories for (final list in itemsByCategory.values) { list.sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); } final categoryIds = itemsByCategory.keys.toList()..sort(); return Container( margin: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.grey.shade900.withAlpha(200), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Service point selector (if multiple) if (servicePoints != null && servicePoints!.length > 1) Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( "Select a table to order", style: TextStyle( color: Colors.white.withAlpha(180), fontSize: 14, ), ), ), // Category sections with items ...categoryIds.map((categoryId) { final items = itemsByCategory[categoryId]!; final categoryName = items.first.categoryName; return _CategorySection( categoryId: categoryId, categoryName: categoryName, items: items, imageBaseUrl: imageBaseUrl, restaurant: restaurant, servicePoints: servicePoints, ); }), const SizedBox(height: 12), ], ), ); } } class _CategorySection extends StatelessWidget { final int categoryId; final String categoryName; final List items; final String imageBaseUrl; final Restaurant restaurant; final List? servicePoints; const _CategorySection({ required this.categoryId, required this.categoryName, required this.items, required this.imageBaseUrl, required this.restaurant, required this.servicePoints, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Category header Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( categoryName, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), // Horizontal scroll of items SizedBox( height: 140, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12), itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return _MenuItemCard( item: item, imageBaseUrl: imageBaseUrl, onTap: () => _handleItemTap(context, item), ); }, ), ), ], ); } void _handleItemTap(BuildContext context, MenuItem item) { // Default to first service point if available final servicePointId = servicePoints?.firstOrNull?.servicePointId ?? 1; // Set app state final appState = context.read(); appState.setBusinessAndServicePoint( restaurant.businessId, servicePointId, businessName: restaurant.name, servicePointName: servicePoints?.firstOrNull?.name, ); Api.setBusinessId(restaurant.businessId); // Navigate to full menu browse screen Navigator.of(context).pushReplacementNamed( '/menu_browse', arguments: { 'businessId': restaurant.businessId, 'servicePointId': servicePointId, }, ); } } class _MenuItemCard extends StatelessWidget { final MenuItem item; final String imageBaseUrl; final VoidCallback onTap; const _MenuItemCard({ required this.item, required this.imageBaseUrl, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 120, margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: Colors.grey.shade800, borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Item image ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), ), child: SizedBox( height: 80, width: 120, child: Image.network( "$imageBaseUrl/items/${item.itemId}.png", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Image.network( "$imageBaseUrl/items/${item.itemId}.jpg", fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( color: Theme.of(context).colorScheme.primaryContainer, child: Center( child: Icon( Icons.restaurant, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32, ), ), ); }, ); }, ), ), ), // Item details Expanded( child: Padding( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.name, style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const Spacer(), Text( "\$${item.price.toStringAsFixed(2)}", style: TextStyle( color: Theme.of(context).colorScheme.primary, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), ), ], ), ), ); } } class _ErrorPane extends StatelessWidget { final String title; final String message; final int? statusCode; final String? raw; final VoidCallback onRetry; const _ErrorPane({ required this.title, required this.message, required this.statusCode, required this.raw, required this.onRetry, }); @override Widget build(BuildContext context) { return Center( child: SingleChildScrollView( padding: const EdgeInsets.all(18), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Card( color: Colors.grey.shade900, child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.white, ), ), const SizedBox(height: 10), Text(message, style: const TextStyle(color: Colors.white70)), const SizedBox(height: 14), if (statusCode != null) Text("HTTP: $statusCode", style: const TextStyle(color: Colors.white70)), if (raw != null && raw!.trim().isNotEmpty) ...[ const SizedBox(height: 10), const Text("Raw response:", style: TextStyle(color: Colors.white70)), const SizedBox(height: 6), Text(raw!, style: const TextStyle(color: Colors.white54)), ], const SizedBox(height: 14), FilledButton( onPressed: onRetry, child: const Text("Retry"), ), ], ), ), ), ), ), ); } }