// lib/screens/restaurant_select_screen.dart import "package:flutter/material.dart"; import "package:provider/provider.dart"; import "package:geolocator/geolocator.dart"; import "../app/app_state.dart"; import "../models/menu_item.dart"; import "../models/restaurant.dart"; import "../models/service_point.dart"; import "../services/api.dart"; import "../widgets/rescan_button.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 = _loadRestaurantsWithLocation(); // 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> _loadRestaurantsWithLocation() async { double? lat; double? lng; // Try to get user location for distance-based sorting try { final permission = await Geolocator.checkPermission(); if (permission == LocationPermission.always || permission == LocationPermission.whileInUse) { final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 5), ), ); lat = position.latitude; lng = position.longitude; debugPrint('[RestaurantSelect] Got location: $lat, $lng'); } } catch (e) { debugPrint('[RestaurantSelect] Location error (continuing without): $e'); } final raw = await Api.listRestaurantsRaw(lat: lat, lng: lng); _debugLastRaw = raw.rawBody; _debugLastStatus = raw.statusCode; return Api.listRestaurants(lat: lat, lng: lng); } 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).pushNamed( '/menu_browse', arguments: { 'businessId': restaurant.businessId, 'servicePointId': servicePointId, }, ); } Future _onWillPop() async { final shouldExit = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Exit App?"), content: const Text("Are you sure you want to exit?"), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text("Cancel"), ), TextButton( onPressed: () => Navigator.of(context).pop(true), child: const Text("Exit"), ), ], ), ); return shouldExit ?? false; } @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; final shouldExit = await _onWillPop(); if (shouldExit && context.mounted) { Navigator.of(context).pop(); } }, child: Scaffold( backgroundColor: Colors.black, appBar: AppBar( backgroundColor: Colors.black, foregroundColor: Colors.white, title: const Text("Nearby Restaurants"), elevation: 0, actions: const [ RescanButton(iconColor: Colors.white), ], ), 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 = _loadRestaurantsWithLocation()), ); } 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 = _loadRestaurantsWithLocation()), ); } 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 card with header image (matches business selector style) GestureDetector( onTap: onTap, child: Container( height: 120, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withAlpha(100), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Stack( children: [ // Header image background Positioned.fill( 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) { // Fallback to logo centered return Container( color: Colors.grey.shade800, child: Center( child: Image.network( "$imageBaseUrl/logos/${restaurant.businessId}.png", width: 60, height: 60, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { return Image.network( "$imageBaseUrl/logos/${restaurant.businessId}.jpg", width: 60, height: 60, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { return Text( restaurant.name.isNotEmpty ? restaurant.name[0].toUpperCase() : "?", style: const TextStyle( color: Colors.white54, fontSize: 36, fontWeight: FontWeight.bold, ), ); }, ); }, ), ), ); }, ); }, ), ), // Gradient overlay Positioned.fill( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withAlpha(180), ], ), ), ), ), // Business name and arrow at bottom Positioned( bottom: 12, left: 16, right: 16, child: Row( children: [ Expanded( child: Text( restaurant.name, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, shadows: [ Shadow( offset: Offset(0, 1), blurRadius: 3, color: Colors.black54, ), ], ), ), ), const Icon( Icons.arrow_forward_ios, color: Colors.white70, size: 20, ), ], ), ), ], ), ), ), ), // 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).pushNamed( '/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"), ), ], ), ), ), ), ), ); } }