When a user taps on a child business from the business selector (beacon flow), now checks if they have an existing cart for that specific business before proceeding. Shows "Existing Order Found" dialog with options to continue or start fresh, matching the behavior from login flow. Fixes bug where existing cart wasn't detected because we were clearing cart state without checking the database first. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
520 lines
18 KiB
Dart
520 lines
18 KiB
Dart
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 "../services/api.dart";
|
|
import "../services/auth_storage.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) {
|
|
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) async {
|
|
// Check if user is logged in and has an existing cart for this business
|
|
final auth = await AuthStorage.loadAuth();
|
|
if (auth != null && auth.userId > 0) {
|
|
try {
|
|
final existingCart = await Api.getActiveCart(userId: auth.userId);
|
|
if (existingCart != null && existingCart.hasItems && existingCart.businessId == business.businessId) {
|
|
// Show existing cart dialog
|
|
if (!mounted) return;
|
|
_showExistingCartDialog(existingCart, business, parentBusinessId, parentBusinessName);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// Ignore - proceed without cart check
|
|
}
|
|
}
|
|
|
|
if (!mounted) return;
|
|
_proceedToMenu(business, parentBusinessId, parentBusinessName);
|
|
}
|
|
|
|
void _showExistingCartDialog(ActiveCartInfo cart, _BusinessItem business, int? parentBusinessId, String? parentBusinessName) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text("Existing Order Found"),
|
|
content: Text(
|
|
"You have ${cart.itemCount} item${cart.itemCount == 1 ? '' : 's'} in your cart at ${cart.businessName}.\n\nWould you like to continue that order or start fresh?",
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.of(ctx).pop();
|
|
// Abandon the old order and proceed with clean cart
|
|
try {
|
|
await Api.abandonOrder(orderId: cart.orderId);
|
|
} catch (e) {
|
|
// Ignore - proceed anyway
|
|
}
|
|
if (!mounted) return;
|
|
final appState = context.read<AppState>();
|
|
appState.clearCart();
|
|
_proceedToMenu(business, parentBusinessId, parentBusinessName);
|
|
},
|
|
child: const Text("Start Fresh"),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(ctx).pop();
|
|
// Continue existing order - load cart and go to menu
|
|
if (!mounted) return;
|
|
final appState = context.read<AppState>();
|
|
appState.setBusinessAndServicePoint(
|
|
cart.businessId,
|
|
cart.servicePointId,
|
|
businessName: cart.businessName,
|
|
servicePointName: cart.servicePointName,
|
|
parentBusinessId: parentBusinessId,
|
|
parentBusinessName: parentBusinessName,
|
|
);
|
|
appState.setCartOrder(
|
|
orderId: cart.orderId,
|
|
orderUuid: cart.orderUuid,
|
|
itemCount: cart.itemCount,
|
|
);
|
|
appState.setOrderType(OrderType.dineIn);
|
|
Api.setBusinessId(cart.businessId);
|
|
|
|
Navigator.of(context).pushReplacementNamed(
|
|
AppRoutes.menuBrowse,
|
|
arguments: {
|
|
"businessId": cart.businessId,
|
|
"servicePointId": cart.servicePointId,
|
|
},
|
|
);
|
|
},
|
|
child: const Text("Continue Order"),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _proceedToMenu(_BusinessItem business, int? parentBusinessId, String? parentBusinessName) {
|
|
final appState = context.read<AppState>();
|
|
|
|
// Clear any existing cart (for different business)
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|