Add custom tip, order type selection, and menu improvements

Cart/Payment:
- Custom tip option (0-200%) with dialog input
- Tip buttons now include "Custom" option
- Default custom tip starts at 25%

Order Flow:
- New order type selection screen (Dine-In, Takeout, Delivery)
- Group order invite screen (placeholder)
- App router updated with new routes

Menu Display:
- Category headers now text-only (removed image loading)
- Simplified category background with gradient and styled text

Beacon Scanning:
- Improved beacon detection flow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-07 18:09:20 -08:00
parent c3814d87e7
commit 29646a8a04
7 changed files with 651 additions and 58 deletions

View file

@ -2,8 +2,10 @@ import "package:flutter/material.dart";
import "../screens/beacon_scan_screen.dart"; import "../screens/beacon_scan_screen.dart";
import "../screens/cart_view_screen.dart"; import "../screens/cart_view_screen.dart";
import "../screens/group_order_invite_screen.dart";
import "../screens/login_screen.dart"; import "../screens/login_screen.dart";
import "../screens/menu_browse_screen.dart"; import "../screens/menu_browse_screen.dart";
import "../screens/order_type_select_screen.dart";
import "../screens/restaurant_select_screen.dart"; import "../screens/restaurant_select_screen.dart";
import "../screens/service_point_select_screen.dart"; import "../screens/service_point_select_screen.dart";
import "../screens/splash_screen.dart"; import "../screens/splash_screen.dart";
@ -12,6 +14,8 @@ class AppRoutes {
static const String splash = "/"; static const String splash = "/";
static const String login = "/login"; static const String login = "/login";
static const String beaconScan = "/beacon-scan"; static const String beaconScan = "/beacon-scan";
static const String orderTypeSelect = "/order-type";
static const String groupOrderInvite = "/group-invite";
static const String restaurantSelect = "/restaurants"; static const String restaurantSelect = "/restaurants";
static const String servicePointSelect = "/service-points"; static const String servicePointSelect = "/service-points";
static const String menuBrowse = "/menu"; static const String menuBrowse = "/menu";
@ -21,6 +25,8 @@ class AppRoutes {
splash: (_) => const SplashScreen(), splash: (_) => const SplashScreen(),
login: (_) => const LoginScreen(), login: (_) => const LoginScreen(),
beaconScan: (_) => const BeaconScanScreen(), beaconScan: (_) => const BeaconScanScreen(),
orderTypeSelect: (_) => const OrderTypeSelectScreen(),
groupOrderInvite: (_) => const GroupOrderInviteScreen(),
restaurantSelect: (_) => const RestaurantSelectScreen(), restaurantSelect: (_) => const RestaurantSelectScreen(),
servicePointSelect: (_) => const ServicePointSelectScreen(), servicePointSelect: (_) => const ServicePointSelectScreen(),
menuBrowse: (_) => const MenuBrowseScreen(), menuBrowse: (_) => const MenuBrowseScreen(),

View file

@ -1,5 +1,11 @@
import "package:flutter/foundation.dart"; import "package:flutter/foundation.dart";
enum OrderType {
dineIn, // Table service (beacon detected)
delivery, // Delivery to customer
takeaway, // Pickup at counter
}
class AppState extends ChangeNotifier { class AppState extends ChangeNotifier {
int? _selectedBusinessId; int? _selectedBusinessId;
String? _selectedBusinessName; String? _selectedBusinessName;
@ -8,6 +14,8 @@ class AppState extends ChangeNotifier {
int? _userId; int? _userId;
OrderType? _orderType;
int? _cartOrderId; int? _cartOrderId;
String? _cartOrderUuid; String? _cartOrderUuid;
int _cartItemCount = 0; int _cartItemCount = 0;
@ -23,13 +31,18 @@ class AppState extends ChangeNotifier {
int? get userId => _userId; int? get userId => _userId;
bool get isLoggedIn => _userId != null && _userId! > 0; bool get isLoggedIn => _userId != null && _userId! > 0;
OrderType? get orderType => _orderType;
bool get isDelivery => _orderType == OrderType.delivery;
bool get isTakeaway => _orderType == OrderType.takeaway;
bool get isDineIn => _orderType == OrderType.dineIn;
int? get cartOrderId => _cartOrderId; int? get cartOrderId => _cartOrderId;
String? get cartOrderUuid => _cartOrderUuid; String? get cartOrderUuid => _cartOrderUuid;
int get cartItemCount => _cartItemCount; int get cartItemCount => _cartItemCount;
int? get activeOrderId => _activeOrderId; int? get activeOrderId => _activeOrderId;
int? get activeOrderStatusId => _activeOrderStatusId; int? get activeOrderStatusId => _activeOrderStatusId;
bool get hasActiveOrder => _activeOrderId != null;
bool get hasLocationSelection => bool get hasLocationSelection =>
_selectedBusinessId != null && _selectedServicePointId != null; _selectedBusinessId != null && _selectedServicePointId != null;
@ -83,6 +96,11 @@ class AppState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setOrderType(OrderType? type) {
_orderType = type;
notifyListeners();
}
void setCartOrder({required int orderId, required String orderUuid, int itemCount = 0}) { void setCartOrder({required int orderId, required String orderUuid, int itemCount = 0}) {
_cartOrderId = orderId; _cartOrderId = orderId;
_cartOrderUuid = orderUuid; _cartOrderUuid = orderUuid;
@ -122,6 +140,7 @@ class AppState extends ChangeNotifier {
void clearAll() { void clearAll() {
_selectedBusinessId = null; _selectedBusinessId = null;
_selectedServicePointId = null; _selectedServicePointId = null;
_orderType = null;
_cartOrderId = null; _cartOrderId = null;
_cartOrderUuid = null; _cartOrderUuid = null;

View file

@ -93,8 +93,8 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
} }
if (_uuidToBeaconId.isEmpty) { if (_uuidToBeaconId.isEmpty) {
print('[BeaconScan] ⚠️ No beacons in database, going to restaurant select'); print('[BeaconScan] ⚠️ No beacons in database, going to order type select');
if (mounted) _navigateToRestaurantSelect(); if (mounted) _navigateToOrderTypeSelect();
return; return;
} }
@ -234,7 +234,7 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
if (beaconScores.isEmpty) { if (beaconScores.isEmpty) {
setState(() => _status = 'No beacons nearby'); setState(() => _status = 'No beacons nearby');
await Future.delayed(const Duration(milliseconds: 800)); await Future.delayed(const Duration(milliseconds: 800));
if (mounted) _navigateToRestaurantSelect(); if (mounted) _navigateToOrderTypeSelect();
} else { } else {
// Find beacon with highest average RSSI and minimum detections // Find beacon with highest average RSSI and minimum detections
final best = _findBestBeacon(beaconScores); final best = _findBestBeacon(beaconScores);
@ -246,7 +246,7 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
print('[BeaconScan] ⚠️ No beacon met minimum confidence threshold'); print('[BeaconScan] ⚠️ No beacon met minimum confidence threshold');
setState(() => _status = 'No strong beacon signal'); setState(() => _status = 'No strong beacon signal');
await Future.delayed(const Duration(milliseconds: 800)); await Future.delayed(const Duration(milliseconds: 800));
if (mounted) _navigateToRestaurantSelect(); if (mounted) _navigateToOrderTypeSelect();
} }
} }
} catch (e) { } catch (e) {
@ -384,6 +384,10 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect); Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
} }
void _navigateToOrderTypeSelect() {
Navigator.of(context).pushReplacementNamed(AppRoutes.orderTypeSelect);
}
void _retryPermissions() async { void _retryPermissions() async {
await BeaconPermissions.openSettings(); await BeaconPermissions.openSettings();
} }

View file

@ -34,13 +34,19 @@ class _CartViewScreenState extends State<CartViewScreen> {
String? _error; String? _error;
Map<int, MenuItem> _menuItemsById = {}; Map<int, MenuItem> _menuItemsById = {};
// Tip options as percentages // Tip options as percentages (null = custom)
static const List<int> _tipPercentages = [0, 15, 18, 20, 25]; static const List<int?> _tipPercentages = [0, 15, 18, 20, null];
int _selectedTipIndex = 1; // Default to 15% int _selectedTipIndex = 1; // Default to 15%
int _customTipPercent = 25; // Default custom tip if selected
double get _tipAmount { double get _tipAmount {
if (_cart == null) return 0.0; if (_cart == null) return 0.0;
return _cart!.subtotal * (_tipPercentages[_selectedTipIndex] / 100); final percent = _tipPercentages[_selectedTipIndex];
if (percent == null) {
// Custom tip
return _cart!.subtotal * (_customTipPercent / 100);
}
return _cart!.subtotal * (percent / 100);
} }
FeeBreakdown get _feeBreakdown { FeeBreakdown get _feeBreakdown {
@ -175,6 +181,57 @@ class _CartViewScreenState extends State<CartViewScreen> {
} }
} }
Future<void> _showCustomTipDialog() async {
final controller = TextEditingController(text: _customTipPercent.toString());
final result = await showDialog<int>(
context: context,
builder: (context) => AlertDialog(
title: const Text("Custom Tip"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
keyboardType: TextInputType.number,
autofocus: true,
decoration: const InputDecoration(
labelText: "Tip Percentage",
suffixText: "%",
hintText: "0-200",
),
),
const SizedBox(height: 8),
Text(
"Enter a tip percentage from 0% to 200%",
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
final value = int.tryParse(controller.text) ?? 0;
final clampedValue = value.clamp(0, 200);
Navigator.pop(context, clampedValue);
},
child: const Text("Apply"),
),
],
),
);
if (result != null) {
setState(() {
_customTipPercent = result;
_selectedTipIndex = _tipPercentages.length - 1; // Select "Custom"
});
}
}
Future<void> _processPaymentAndSubmit() async { Future<void> _processPaymentAndSubmit() async {
if (_cart == null) return; if (_cart == null) return;
@ -619,6 +676,7 @@ class _CartViewScreenState extends State<CartViewScreen> {
children: List.generate(_tipPercentages.length, (index) { children: List.generate(_tipPercentages.length, (index) {
final isSelected = _selectedTipIndex == index; final isSelected = _selectedTipIndex == index;
final percent = _tipPercentages[index]; final percent = _tipPercentages[index];
final isCustom = percent == null;
return Expanded( return Expanded(
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -626,7 +684,13 @@ class _CartViewScreenState extends State<CartViewScreen> {
right: index == _tipPercentages.length - 1 ? 0 : 4, right: index == _tipPercentages.length - 1 ? 0 : 4,
), ),
child: GestureDetector( child: GestureDetector(
onTap: () => setState(() => _selectedTipIndex = index), onTap: () {
if (isCustom) {
_showCustomTipDialog();
} else {
setState(() => _selectedTipIndex = index);
}
},
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -638,7 +702,9 @@ class _CartViewScreenState extends State<CartViewScreen> {
), ),
child: Center( child: Center(
child: Text( child: Text(
percent == 0 ? "No tip" : "$percent%", isCustom
? (isSelected ? "$_customTipPercent%" : "Custom")
: (percent == 0 ? "No tip" : "$percent%"),
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,

View file

@ -0,0 +1,303 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app/app_router.dart';
import '../app/app_state.dart';
/// Screen to invite additional Payfrit users to join a group order
/// Shown after selecting Delivery or Takeaway
class GroupOrderInviteScreen extends StatefulWidget {
const GroupOrderInviteScreen({super.key});
@override
State<GroupOrderInviteScreen> createState() => _GroupOrderInviteScreenState();
}
class _GroupOrderInviteScreenState extends State<GroupOrderInviteScreen> {
final List<String> _invitedUsers = [];
final TextEditingController _searchController = TextEditingController();
bool _isSearching = false;
// Mock search results - in production this would come from API
final List<_UserResult> _searchResults = [];
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _searchUsers(String query) {
if (query.isEmpty) {
setState(() {
_searchResults.clear();
_isSearching = false;
});
return;
}
setState(() => _isSearching = true);
// TODO: Replace with actual API call to search users by phone/email/username
// For now, show placeholder
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted && _searchController.text == query) {
setState(() {
_searchResults.clear();
// Mock results - would come from API
if (query.length >= 3) {
_searchResults.addAll([
_UserResult(
userId: 1,
name: 'John D.',
phone: '***-***-${query.substring(0, 4)}',
),
]);
}
_isSearching = false;
});
}
});
}
void _inviteUser(_UserResult user) {
if (!_invitedUsers.contains(user.name)) {
setState(() {
_invitedUsers.add(user.name);
_searchResults.clear();
_searchController.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Invitation sent to ${user.name}'),
backgroundColor: Colors.green,
),
);
}
}
void _removeInvite(String userName) {
setState(() {
_invitedUsers.remove(userName);
});
}
void _continueToRestaurants() {
// Store invited users in app state if needed
final appState = context.read<AppState>();
// TODO: appState.setGroupOrderInvites(_invitedUsers);
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
}
void _skipInvites() {
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
final orderTypeLabel = appState.isDelivery ? 'Delivery' : 'Takeaway';
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
title: Text('$orderTypeLabel Order'),
elevation: 0,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
const Icon(
Icons.group_add,
color: Colors.white54,
size: 48,
),
const SizedBox(height: 16),
const Text(
"Invite others to join",
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
"Add Payfrit users to share this order.\n${appState.isDelivery ? 'Split the delivery fee between everyone!' : 'Everyone pays for their own items.'}",
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Search field
TextField(
controller: _searchController,
onChanged: _searchUsers,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Search by phone or email...',
hintStyle: const TextStyle(color: Colors.white38),
prefixIcon: const Icon(Icons.search, color: Colors.white54),
suffixIcon: _isSearching
? const Padding(
padding: EdgeInsets.all(12),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white54,
),
),
)
: null,
filled: true,
fillColor: Colors.grey.shade900,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
// Search results
if (_searchResults.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: _searchResults.map((user) {
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.withAlpha(50),
child: Text(
user.name[0].toUpperCase(),
style: const TextStyle(color: Colors.blue),
),
),
title: Text(
user.name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
user.phone,
style: const TextStyle(color: Colors.white54),
),
trailing: IconButton(
icon: const Icon(Icons.person_add, color: Colors.blue),
onPressed: () => _inviteUser(user),
),
);
}).toList(),
),
),
],
const SizedBox(height: 16),
// Invited users list
if (_invitedUsers.isNotEmpty) ...[
const Text(
'Invited:',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _invitedUsers.map((name) {
return Chip(
avatar: CircleAvatar(
backgroundColor: Colors.green.withAlpha(50),
child: const Icon(
Icons.check,
size: 16,
color: Colors.green,
),
),
label: Text(name),
deleteIcon: const Icon(Icons.close, size: 18),
onDeleted: () => _removeInvite(name),
backgroundColor: Colors.grey.shade800,
labelStyle: const TextStyle(color: Colors.white),
);
}).toList(),
),
],
const Spacer(),
// Continue button
if (_invitedUsers.isNotEmpty)
FilledButton.icon(
onPressed: _continueToRestaurants,
icon: const Icon(Icons.group),
label: Text('Continue with ${_invitedUsers.length + 1} people'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
)
else
FilledButton(
onPressed: _continueToRestaurants,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Continue Alone'),
),
const SizedBox(height: 12),
// Skip button
TextButton(
onPressed: _skipInvites,
child: const Text(
'Skip this step',
style: TextStyle(color: Colors.white54),
),
),
],
),
),
),
);
}
}
class _UserResult {
final int userId;
final String name;
final String phone;
const _UserResult({
required this.userId,
required this.name,
required this.phone,
});
}

View file

@ -469,54 +469,39 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
); );
} }
/// Builds category background - tries image first, falls back to styled text /// Builds category background - styled text only (no images)
Widget _buildCategoryBackground(int categoryId, String categoryName) { Widget _buildCategoryBackground(int categoryId, String categoryName) {
return Image.network( const darkForestGreen = Color(0xFF1B4D3E);
"$_imageBaseUrl/categories/$categoryId.png", return Container(
fit: BoxFit.cover, decoration: BoxDecoration(
semanticLabel: categoryName, gradient: LinearGradient(
errorBuilder: (context, error, stackTrace) { begin: Alignment.topCenter,
return Image.network( end: Alignment.bottomCenter,
"$_imageBaseUrl/categories/$categoryId.jpg", colors: [
fit: BoxFit.cover, Colors.white,
semanticLabel: categoryName, Colors.grey.shade100,
errorBuilder: (context, error, stackTrace) { ],
// No image - show white background with dark forest green text ),
const darkForestGreen = Color(0xFF1B4D3E); ),
return Container( alignment: Alignment.center,
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(horizontal: 24),
gradient: LinearGradient( child: Text(
begin: Alignment.topCenter, categoryName,
end: Alignment.bottomCenter, textAlign: TextAlign.center,
colors: [ style: const TextStyle(
Colors.white, fontSize: 28,
Colors.grey.shade100, fontWeight: FontWeight.bold,
], color: darkForestGreen,
), letterSpacing: 1.2,
), shadows: [
alignment: Alignment.center, Shadow(
padding: const EdgeInsets.symmetric(horizontal: 24), offset: Offset(1, 1),
child: Text( blurRadius: 2,
categoryName, color: Colors.black26,
textAlign: TextAlign.center, ),
style: const TextStyle( ],
fontSize: 28, ),
fontWeight: FontWeight.bold, ),
color: darkForestGreen,
letterSpacing: 1.2,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2,
color: Colors.black26,
),
],
),
),
);
},
);
},
); );
} }
@ -1006,11 +991,21 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> {
bool _validate() { bool _validate() {
setState(() => _validationError = null); setState(() => _validationError = null);
// Helper to check if a modifier group has any selected descendants
bool hasSelectedDescendant(int itemId) {
if (_selectedItemIds.contains(itemId)) return true;
final children = widget.itemsByParent[itemId] ?? [];
return children.any((c) => hasSelectedDescendant(c.itemId));
}
bool validateRecursive(int parentId, MenuItem parent) { bool validateRecursive(int parentId, MenuItem parent) {
final children = widget.itemsByParent[parentId] ?? []; final children = widget.itemsByParent[parentId] ?? [];
if (children.isEmpty) return true; if (children.isEmpty) return true;
final selectedChildren = children.where((c) => _selectedItemIds.contains(c.itemId)).toList(); // A child is "selected" if it's directly selected OR if it's a group with selected descendants
final selectedChildren = children.where((c) =>
_selectedItemIds.contains(c.itemId) || hasSelectedDescendant(c.itemId)
).toList();
// Check if child selection is required // Check if child selection is required
if (parent.requiresChildSelection && selectedChildren.isEmpty) { if (parent.requiresChildSelection && selectedChildren.isEmpty) {

View file

@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app/app_router.dart';
import '../app/app_state.dart';
/// Screen shown when no beacon is detected
/// Allows user to choose between Delivery or Takeaway
class OrderTypeSelectScreen extends StatelessWidget {
const OrderTypeSelectScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(flex: 1),
// Header
const Icon(
Icons.bluetooth_disabled,
color: Colors.white54,
size: 64,
),
const SizedBox(height: 24),
const Text(
"No beacons found",
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
const Text(
"Are you ordering for:",
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
textAlign: TextAlign.center,
),
const Spacer(flex: 1),
// Delivery option
_OrderTypeCard(
icon: Icons.delivery_dining,
title: "Delivery",
subtitle: "Have your order delivered to you",
color: Colors.blue,
onTap: () => _selectOrderType(context, OrderType.delivery),
),
const SizedBox(height: 16),
// Takeaway option
_OrderTypeCard(
icon: Icons.shopping_bag_outlined,
title: "Takeaway",
subtitle: "Pick up your order at the counter",
color: Colors.orange,
onTap: () => _selectOrderType(context, OrderType.takeaway),
),
const Spacer(flex: 2),
// Skip / manual selection
TextButton(
onPressed: () => _navigateToRestaurantSelect(context),
child: const Text(
"Select Restaurant Manually",
style: TextStyle(
color: Colors.white54,
fontSize: 14,
),
),
),
const SizedBox(height: 16),
],
),
),
),
);
}
void _selectOrderType(BuildContext context, OrderType orderType) {
final appState = context.read<AppState>();
appState.setOrderType(orderType);
// Navigate to group invite screen to optionally add other users
Navigator.of(context).pushReplacementNamed(AppRoutes.groupOrderInvite);
}
void _navigateToRestaurantSelect(BuildContext context) {
// Clear any order type and go to restaurant selection
final appState = context.read<AppState>();
appState.setOrderType(null);
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
}
}
class _OrderTypeCard extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final Color color;
final VoidCallback onTap;
const _OrderTypeCard({
required this.icon,
required this.title,
required this.subtitle,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: color.withAlpha(100),
width: 2,
),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
color.withAlpha(30),
color.withAlpha(10),
],
),
),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: color.withAlpha(50),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 32,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: color,
size: 28,
),
],
),
),
),
);
}
}