payfrit-app/lib/screens/business_selector_screen.dart
John Mizerek c4792189dd App Store Version 2: Beacon scanning, preload caching, business selector
Features:
- Beacon scanner service for detecting nearby beacons
- Beacon cache for offline-first beacon resolution
- Preload cache for instant menu display
- Business selector screen for multi-location support
- Rescan button widget for quick beacon refresh
- Sign-in dialog for guest checkout flow
- Task type model for server tasks

Improvements:
- Enhanced menu browsing with category filtering
- Improved cart view with better modifier display
- Order history with detailed order tracking
- Chat screen improvements
- Better error handling in API service

Fixes:
- CashApp payment return crash fix
- Modifier nesting issues resolved
- Auto-expand modifier groups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 19:51:54 -08:00

435 lines
14 KiB
Dart

import "package:flutter/material.dart";
import "package:provider/provider.dart";
import "../app/app_router.dart";
import "../app/app_state.dart";
import "../services/api.dart";
import "../widgets/rescan_button.dart";
class BusinessSelectorScreen extends StatefulWidget {
const BusinessSelectorScreen({super.key});
@override
State<BusinessSelectorScreen> createState() => _BusinessSelectorScreenState();
}
class _BusinessSelectorScreenState extends State<BusinessSelectorScreen> {
static const String _imageBaseUrl = "https://biz.payfrit.com/uploads";
String? _parentName;
int? _parentBusinessId;
int? _servicePointId;
String? _servicePointName;
List<_BusinessItem>? _businesses;
bool _loading = false;
bool _initialized = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) {
_initialized = true;
_initializeData();
}
}
Future<void> _initializeData() async {
final args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
final BeaconBusinessMapping? mapping = args?["mapping"] as BeaconBusinessMapping?;
final List<ChildBusiness>? children = args?["children"] as List<ChildBusiness>?;
if (mapping != null && mapping.businesses.isNotEmpty) {
// From beacon mapping
setState(() {
_parentName = mapping.parent?.businessName ?? mapping.businessName;
_parentBusinessId = mapping.parent?.businessId ?? mapping.businessId;
_servicePointId = mapping.servicePointId;
_servicePointName = mapping.servicePointName;
_businesses = mapping.businesses.map((b) => _BusinessItem(
businessId: b.businessId,
businessName: b.businessName,
servicePointId: b.servicePointId,
servicePointName: b.servicePointName,
)).toList();
});
} else if (children != null && children.isNotEmpty) {
// From menu_browse_screen with children list
setState(() {
_parentName = (args?["parentBusinessName"] as String?) ?? "this location";
_parentBusinessId = args?["parentBusinessId"] as int?;
_servicePointId = args?["servicePointId"] as int?;
_servicePointName = args?["servicePointName"] as String?;
_businesses = children.map((c) => _BusinessItem(
businessId: c.businessId,
businessName: c.businessName,
servicePointId: _servicePointId ?? 0,
servicePointName: _servicePointName ?? "",
)).toList();
});
} else if (args?["parentBusinessId"] != null) {
// From menu back button - need to fetch children
_parentBusinessId = args?["parentBusinessId"] as int?;
_parentName = args?["parentBusinessName"] as String? ?? "this location";
_servicePointId = args?["servicePointId"] as int?;
_servicePointName = args?["servicePointName"] as String?;
setState(() => _loading = true);
try {
final fetchedChildren = await Api.getChildBusinesses(businessId: _parentBusinessId!);
if (mounted) {
setState(() {
_businesses = fetchedChildren.map((c) => _BusinessItem(
businessId: c.businessId,
businessName: c.businessName,
servicePointId: _servicePointId ?? 0,
servicePointName: _servicePointName ?? "",
)).toList();
_loading = false;
});
}
} catch (e) {
if (mounted) {
setState(() => _loading = false);
}
}
}
}
@override
Widget build(BuildContext context) {
if (_loading) {
return Scaffold(
backgroundColor: Colors.black,
body: const Center(
child: CircularProgressIndicator(color: Colors.white),
),
);
}
if (_businesses == null || _businesses!.isEmpty) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"No businesses available",
style: TextStyle(color: Colors.white, fontSize: 18),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect),
child: const Text("Browse Restaurants"),
),
],
),
),
);
}
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Column(
children: [
// Large header banner with back button overlay
_buildHeaderBanner(context, _parentBusinessId, _parentName ?? ""),
// Message
const Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Text(
"Please select one of the businesses below:",
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
// Business list with header images
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _businesses!.length,
itemBuilder: (context, index) {
final business = _businesses![index];
return _BusinessCardWithHeader(
businessId: business.businessId,
businessName: business.businessName,
imageBaseUrl: _imageBaseUrl,
onTap: () => _selectBusiness(context, business, _parentBusinessId, _parentName),
);
},
),
),
],
),
),
);
}
Widget _buildHeaderBanner(BuildContext context, int? parentBusinessId, String parentName) {
const imageBaseUrl = _imageBaseUrl;
return SizedBox(
height: 200,
width: double.infinity,
child: Stack(
children: [
// Background image
if (parentBusinessId != null)
Positioned.fill(
child: Image.network(
"$_imageBaseUrl/headers/$parentBusinessId.png",
fit: BoxFit.fitWidth,
errorBuilder: (context, error, stackTrace) {
return Image.network(
"$_imageBaseUrl/headers/$parentBusinessId.jpg",
fit: BoxFit.fitWidth,
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.grey.shade800,
Colors.grey.shade900,
],
),
),
);
},
);
},
),
),
// Subtle gradient overlay at bottom for readability of text below
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 40,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withAlpha(150),
],
),
),
),
),
// Back button
Positioned(
top: 8,
left: 8,
child: IconButton(
onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect),
icon: const Icon(Icons.arrow_back),
color: Colors.white,
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
),
),
),
// Rescan button
Positioned(
top: 8,
right: 8,
child: Container(
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(20),
),
child: const RescanButton(iconColor: Colors.white),
),
),
],
),
);
}
void _selectBusiness(BuildContext context, _BusinessItem business, int? parentBusinessId, String? parentBusinessName) {
final appState = context.read<AppState>();
// Clear any existing cart
appState.clearCart();
// Set the selected business and service point (with parent info for back navigation)
appState.setBusinessAndServicePoint(
business.businessId,
business.servicePointId,
businessName: business.businessName,
servicePointName: business.servicePointName,
parentBusinessId: parentBusinessId,
parentBusinessName: parentBusinessName,
);
appState.setOrderType(OrderType.dineIn);
Api.setBusinessId(business.businessId);
// Navigate to menu
Navigator.of(context).pushReplacementNamed(
AppRoutes.menuBrowse,
arguments: {
"businessId": business.businessId,
"servicePointId": business.servicePointId,
},
);
}
}
/// Internal business item for unified handling
class _BusinessItem {
final int businessId;
final String businessName;
final int servicePointId;
final String servicePointName;
const _BusinessItem({
required this.businessId,
required this.businessName,
required this.servicePointId,
required this.servicePointName,
});
}
/// Business card with header image background
class _BusinessCardWithHeader extends StatelessWidget {
final int businessId;
final String businessName;
final String imageBaseUrl;
final VoidCallback onTap;
const _BusinessCardWithHeader({
required this.businessId,
required this.businessName,
required this.imageBaseUrl,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 120,
margin: const EdgeInsets.only(bottom: 16),
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/$businessId.png",
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.network(
"$imageBaseUrl/headers/$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/$businessId.png",
width: 60,
height: 60,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Image.network(
"$imageBaseUrl/logos/$businessId.jpg",
width: 60,
height: 60,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Text(
businessName.isNotEmpty ? businessName[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(
businessName,
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,
),
],
),
),
],
),
),
),
);
}
}