payfrit-app/lib/screens/business_selector_screen.dart
John Mizerek 3e68a3282b Check for existing cart when selecting child business from beacon
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>
2026-01-26 10:37:41 -08:00

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,
),
],
),
),
],
),
),
),
);
}
}