Display cart modifiers with category breadcrumbs
- OrderLineItem now includes itemName, itemParentName, and isCheckedByDefault from API response - Cart view displays modifiers as "Category: Selection" format (e.g., "Select Drink: Coke") instead of just item IDs - No longer requires menu item lookup for modifier names Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
867d67c8eb
commit
ef8421c88a
6 changed files with 331 additions and 22 deletions
|
|
@ -161,6 +161,10 @@ class OrderLineItem {
|
||||||
final String? remark;
|
final String? remark;
|
||||||
final bool isDeleted;
|
final bool isDeleted;
|
||||||
final DateTime addedOn;
|
final DateTime addedOn;
|
||||||
|
final String? itemName;
|
||||||
|
final int? itemParentItemId;
|
||||||
|
final String? itemParentName;
|
||||||
|
final bool isCheckedByDefault;
|
||||||
|
|
||||||
const OrderLineItem({
|
const OrderLineItem({
|
||||||
required this.orderLineItemId,
|
required this.orderLineItemId,
|
||||||
|
|
@ -173,6 +177,10 @@ class OrderLineItem {
|
||||||
this.remark,
|
this.remark,
|
||||||
required this.isDeleted,
|
required this.isDeleted,
|
||||||
required this.addedOn,
|
required this.addedOn,
|
||||||
|
this.itemName,
|
||||||
|
this.itemParentItemId,
|
||||||
|
this.itemParentName,
|
||||||
|
this.isCheckedByDefault = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory OrderLineItem.fromJson(Map<String, dynamic> json) {
|
factory OrderLineItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -187,6 +195,10 @@ class OrderLineItem {
|
||||||
remark: json["OrderLineItemRemark"] as String?,
|
remark: json["OrderLineItemRemark"] as String?,
|
||||||
isDeleted: _parseBool(json["OrderLineItemIsDeleted"]),
|
isDeleted: _parseBool(json["OrderLineItemIsDeleted"]),
|
||||||
addedOn: _parseDateTime(json["OrderLineItemAddedOn"]),
|
addedOn: _parseDateTime(json["OrderLineItemAddedOn"]),
|
||||||
|
itemName: json["ItemName"] as String?,
|
||||||
|
itemParentItemId: _parseInt(json["ItemParentItemID"]),
|
||||||
|
itemParentName: json["ItemParentName"] as String?,
|
||||||
|
isCheckedByDefault: _parseBool(json["ItemIsCheckedByDefault"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class OrderDetail {
|
||||||
final OrderCustomer customer;
|
final OrderCustomer customer;
|
||||||
final OrderServicePoint servicePoint;
|
final OrderServicePoint servicePoint;
|
||||||
final List<OrderLineItemDetail> lineItems;
|
final List<OrderLineItemDetail> lineItems;
|
||||||
|
final List<OrderStaff> staff;
|
||||||
|
|
||||||
const OrderDetail({
|
const OrderDetail({
|
||||||
required this.orderId,
|
required this.orderId,
|
||||||
|
|
@ -38,10 +39,12 @@ class OrderDetail {
|
||||||
required this.customer,
|
required this.customer,
|
||||||
required this.servicePoint,
|
required this.servicePoint,
|
||||||
required this.lineItems,
|
required this.lineItems,
|
||||||
|
required this.staff,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory OrderDetail.fromJson(Map<String, dynamic> json) {
|
factory OrderDetail.fromJson(Map<String, dynamic> json) {
|
||||||
final lineItemsJson = json['LineItems'] as List<dynamic>? ?? [];
|
final lineItemsJson = json['LineItems'] as List<dynamic>? ?? [];
|
||||||
|
final staffJson = json['Staff'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
return OrderDetail(
|
return OrderDetail(
|
||||||
orderId: _parseInt(json['OrderID']) ?? 0,
|
orderId: _parseInt(json['OrderID']) ?? 0,
|
||||||
|
|
@ -64,6 +67,9 @@ class OrderDetail {
|
||||||
lineItems: lineItemsJson
|
lineItems: lineItemsJson
|
||||||
.map((e) => OrderLineItemDetail.fromJson(e as Map<String, dynamic>))
|
.map((e) => OrderLineItemDetail.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
staff: staffJson
|
||||||
|
.map((e) => OrderStaff.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,6 +169,26 @@ class OrderServicePoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OrderStaff {
|
||||||
|
final int userId;
|
||||||
|
final String firstName;
|
||||||
|
final String avatarUrl;
|
||||||
|
|
||||||
|
const OrderStaff({
|
||||||
|
required this.userId,
|
||||||
|
required this.firstName,
|
||||||
|
required this.avatarUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory OrderStaff.fromJson(Map<String, dynamic> json) {
|
||||||
|
return OrderStaff(
|
||||||
|
userId: (json['UserID'] as num?)?.toInt() ?? 0,
|
||||||
|
firstName: (json['FirstName'] as String?) ?? '',
|
||||||
|
avatarUrl: (json['AvatarUrl'] as String?) ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class OrderLineItemDetail {
|
class OrderLineItemDetail {
|
||||||
final int lineItemId;
|
final int lineItemId;
|
||||||
final int itemId;
|
final int itemId;
|
||||||
|
|
|
||||||
|
|
@ -129,11 +129,36 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load cart
|
// Load cart
|
||||||
final cart = await Api.getCart(orderId: cartOrderId);
|
var cart = await Api.getCart(orderId: cartOrderId);
|
||||||
|
|
||||||
|
// If cart is not in cart status (0), it's been submitted - clear it and show empty cart
|
||||||
|
if (cart.statusId != 0) {
|
||||||
|
debugPrint('Cart has been submitted (status=${cart.statusId}), clearing cart reference');
|
||||||
|
appState.clearCart();
|
||||||
|
setState(() {
|
||||||
|
_cart = null;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're dine-in (beacon detected) but cart has no order type set, update it
|
||||||
|
if (appState.isDineIn && cart.orderTypeId == 0) {
|
||||||
|
try {
|
||||||
|
cart = await Api.setOrderType(
|
||||||
|
orderId: cart.orderId,
|
||||||
|
orderTypeId: 1, // dine-in
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Log error but continue - cart will show order type selection if this fails
|
||||||
|
debugPrint('Failed to update order type to dine-in: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load menu items to get names and prices
|
// Load menu items to get names and prices
|
||||||
final businessId = appState.selectedBusinessId;
|
// Use cart's businessId if appState doesn't have one (e.g., delivery/takeaway flow)
|
||||||
if (businessId != null) {
|
final businessId = appState.selectedBusinessId ?? cart.businessId;
|
||||||
|
if (businessId > 0) {
|
||||||
final menuItems = await Api.listMenuItems(businessId: businessId);
|
final menuItems = await Api.listMenuItems(businessId: businessId);
|
||||||
_menuItemsById = {for (var item in menuItems) item.itemId: item};
|
_menuItemsById = {for (var item in menuItems) item.itemId: item};
|
||||||
}
|
}
|
||||||
|
|
@ -572,8 +597,9 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRootItemCard(OrderLineItem rootItem) {
|
Widget _buildRootItemCard(OrderLineItem rootItem) {
|
||||||
|
// Use itemName from line item (from API), fall back to menu item lookup, then to ID
|
||||||
final menuItem = _menuItemsById[rootItem.itemId];
|
final menuItem = _menuItemsById[rootItem.itemId];
|
||||||
final itemName = menuItem?.name ?? "Item #${rootItem.itemId}";
|
final itemName = rootItem.itemName ?? menuItem?.name ?? "Item #${rootItem.itemId}";
|
||||||
|
|
||||||
print('[Cart] Building card for root item: $itemName (OrderLineItemID=${rootItem.orderLineItemId})');
|
print('[Cart] Building card for root item: $itemName (OrderLineItemID=${rootItem.orderLineItemId})');
|
||||||
print('[Cart] Total line items in cart: ${_cart!.lineItems.length}');
|
print('[Cart] Total line items in cart: ${_cart!.lineItems.length}');
|
||||||
|
|
@ -688,10 +714,8 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
|
|
||||||
// Recursively collect leaf items with their paths
|
// Recursively collect leaf items with their paths
|
||||||
void collectLeafPaths(OrderLineItem item, List<String> currentPath) {
|
void collectLeafPaths(OrderLineItem item, List<String> currentPath) {
|
||||||
final menuItem = _menuItemsById[item.itemId];
|
|
||||||
|
|
||||||
// Skip default items - they don't need to be repeated in the cart
|
// Skip default items - they don't need to be repeated in the cart
|
||||||
if (menuItem?.isCheckedByDefault == true) {
|
if (item.isCheckedByDefault) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -701,12 +725,18 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
!child.isDeleted)
|
!child.isDeleted)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final itemName = menuItem?.name ?? "Item #${item.itemId}";
|
// Use itemName from line item, fall back to menu item lookup
|
||||||
|
final menuItem = _menuItemsById[item.itemId];
|
||||||
|
final itemName = item.itemName ?? menuItem?.name ?? "Item #${item.itemId}";
|
||||||
|
|
||||||
if (children.isEmpty) {
|
if (children.isEmpty) {
|
||||||
// This is a leaf - add its path
|
// This is a leaf - build display text with parent category name
|
||||||
|
// Format: "Category: Selection" (e.g., "Select Drink: Coke")
|
||||||
|
final displayName = item.itemParentName != null && item.itemParentName!.isNotEmpty
|
||||||
|
? "${item.itemParentName}: $itemName"
|
||||||
|
: itemName;
|
||||||
paths.add(ModifierPath(
|
paths.add(ModifierPath(
|
||||||
names: [...currentPath, itemName],
|
names: [...currentPath, displayName],
|
||||||
price: item.price,
|
price: item.price,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -771,11 +801,16 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// Constrain max height so it doesn't push content off screen
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||||
|
),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
// Order Type Selection (only for delivery/takeaway orders)
|
// Order Type Selection (only for delivery/takeaway orders)
|
||||||
if (_needsOrderTypeSelection) ...[
|
if (_needsOrderTypeSelection) ...[
|
||||||
const Text(
|
const Text(
|
||||||
|
|
@ -947,7 +982,8 @@ class _CartViewScreenState extends State<CartViewScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -239,11 +239,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _phoneController,
|
controller: _phoneController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Phone Number",
|
labelText: "Phone Number",
|
||||||
hintText: "(555) 123-4567",
|
hintText: "(555) 123-4567",
|
||||||
border: OutlineInputBorder(),
|
hintStyle: TextStyle(color: Colors.grey.shade400),
|
||||||
prefixIcon: Icon(Icons.phone),
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.phone),
|
||||||
prefixText: "+1 ",
|
prefixText: "+1 ",
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
|
|
@ -311,11 +312,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _otpController,
|
controller: _otpController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Login Code",
|
labelText: "Login Code",
|
||||||
hintText: "123456",
|
hintText: "123456",
|
||||||
border: OutlineInputBorder(),
|
hintStyle: TextStyle(color: Colors.grey.shade400),
|
||||||
prefixIcon: Icon(Icons.lock),
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.lock),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
|
|
|
||||||
|
|
@ -1015,6 +1015,30 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
} else {
|
} else {
|
||||||
// We have an existing cart ID
|
// We have an existing cart ID
|
||||||
cart = await Api.getCart(orderId: appState.cartOrderId!);
|
cart = await Api.getCart(orderId: appState.cartOrderId!);
|
||||||
|
|
||||||
|
// If cart is not in cart status (0), it's been submitted - create a new cart
|
||||||
|
if (cart.statusId != 0) {
|
||||||
|
debugPrint('Cart has been submitted (status=${cart.statusId}), creating new cart');
|
||||||
|
appState.clearCart();
|
||||||
|
final orderTypeId = appState.isDineIn ? 1 : 0;
|
||||||
|
cart = await Api.getOrCreateCart(
|
||||||
|
userId: _userId!,
|
||||||
|
businessId: _businessId!,
|
||||||
|
servicePointId: _servicePointId!,
|
||||||
|
orderTypeId: orderTypeId,
|
||||||
|
);
|
||||||
|
appState.setCartOrder(
|
||||||
|
orderId: cart.orderId,
|
||||||
|
orderUuid: cart.orderUuid,
|
||||||
|
itemCount: cart.itemCount,
|
||||||
|
);
|
||||||
|
} else if (appState.isDineIn && cart.orderTypeId == 0) {
|
||||||
|
// If we're dine-in (beacon detected) but cart has no order type set, update it
|
||||||
|
cart = await Api.setOrderType(
|
||||||
|
orderId: cart.orderId,
|
||||||
|
orderTypeId: 1, // dine-in
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this item already exists in the cart (as a root item)
|
// Check if this item already exists in the cart (as a root item)
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,10 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||||
_buildOrderHeader(order),
|
_buildOrderHeader(order),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildStatusCard(order),
|
_buildStatusCard(order),
|
||||||
|
if (order.staff.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildStaffCard(order),
|
||||||
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildItemsCard(order),
|
_buildItemsCard(order),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -293,11 +297,15 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||||
icon = Icons.check_circle_outline;
|
icon = Icons.check_circle_outline;
|
||||||
color = Colors.green;
|
color = Colors.green;
|
||||||
break;
|
break;
|
||||||
case 4: // Completed
|
case 4: // Out for Delivery
|
||||||
|
icon = Icons.local_shipping;
|
||||||
|
color = Colors.blue;
|
||||||
|
break;
|
||||||
|
case 5: // Delivered
|
||||||
icon = Icons.check_circle;
|
icon = Icons.check_circle;
|
||||||
color = Colors.green;
|
color = Colors.green;
|
||||||
break;
|
break;
|
||||||
case 5: // Cancelled
|
case 6: // Cancelled
|
||||||
icon = Icons.cancel;
|
icon = Icons.cancel;
|
||||||
color = Colors.red;
|
color = Colors.red;
|
||||||
break;
|
break;
|
||||||
|
|
@ -316,6 +324,207 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildStaffCard(OrderDetail order) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.people,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Your Server${order.staff.length > 1 ? 's' : ''}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: order.staff.map((staff) => _buildStaffItem(staff)).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStaffItem(OrderStaff staff) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _showTipDialog(staff),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 32,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
backgroundImage: staff.avatarUrl.isNotEmpty
|
||||||
|
? NetworkImage(staff.avatarUrl)
|
||||||
|
: null,
|
||||||
|
onBackgroundImageError: staff.avatarUrl.isNotEmpty
|
||||||
|
? (_, __) {} // Silently handle missing images
|
||||||
|
: null,
|
||||||
|
child: staff.avatarUrl.isEmpty
|
||||||
|
? Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 32,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
staff.firstName.isNotEmpty ? staff.firstName : 'Staff',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Tip',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTipDialog(OrderStaff staff) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 40,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
backgroundImage: staff.avatarUrl.isNotEmpty
|
||||||
|
? NetworkImage(staff.avatarUrl)
|
||||||
|
: null,
|
||||||
|
child: staff.avatarUrl.isEmpty
|
||||||
|
? Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 40,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Tip ${staff.firstName.isNotEmpty ? staff.firstName : "Staff"}',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_buildTipButton(staff, 2),
|
||||||
|
_buildTipButton(staff, 5),
|
||||||
|
_buildTipButton(staff, 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => _showCustomTipDialog(staff),
|
||||||
|
child: const Text('Custom Amount'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTipButton(OrderStaff staff, int amount) {
|
||||||
|
return FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_processTip(staff, amount.toDouble());
|
||||||
|
},
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
),
|
||||||
|
child: Text('\$$amount'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCustomTipDialog(OrderStaff staff) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
final controller = TextEditingController();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Tip ${staff.firstName}'),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixText: '\$ ',
|
||||||
|
hintText: '0.00',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
final amount = double.tryParse(controller.text);
|
||||||
|
if (amount != null && amount > 0) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_processTip(staff, amount);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Send Tip'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processTip(OrderStaff staff, double amount) {
|
||||||
|
// TODO: Implement actual tip processing via API
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Tip of \$${amount.toStringAsFixed(2)} for ${staff.firstName} - Coming soon!',
|
||||||
|
style: const TextStyle(color: Colors.black),
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFF90EE90),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: const EdgeInsets.only(bottom: 80, left: 16, right: 16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildItemsCard(OrderDetail order) {
|
Widget _buildItemsCard(OrderDetail order) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue