Implement complete cart management system
Add full cart functionality with API integration: - Created Cart and OrderLineItem models with robust JSON parsing - Implemented cart API methods (getOrCreateCart, setLineItem, getCart, submitOrder) - Added cart state management to AppState with item count tracking - Built cart view screen with item display, quantity editing, and removal - Added cart badge to menu screen showing item count - Implemented real add-to-cart logic with recursive modifier handling - Added category name display in menu browsing - Fixed API response case sensitivity (ORDER/ORDERLINEITEMS) - Enhanced MenuItem model with categoryName field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9995eb2ff7
commit
f505eeb722
7 changed files with 870 additions and 23 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "../screens/cart_view_screen.dart";
|
||||
import "../screens/login_screen.dart";
|
||||
import "../screens/menu_browse_screen.dart";
|
||||
import "../screens/restaurant_select_screen.dart";
|
||||
|
|
@ -12,6 +13,7 @@ class AppRoutes {
|
|||
static const String restaurantSelect = "/restaurants";
|
||||
static const String servicePointSelect = "/service-points";
|
||||
static const String menuBrowse = "/menu";
|
||||
static const String cartView = "/cart";
|
||||
|
||||
static Map<String, WidgetBuilder> get routes => {
|
||||
splash: (_) => const SplashScreen(),
|
||||
|
|
@ -19,5 +21,6 @@ class AppRoutes {
|
|||
restaurantSelect: (_) => const RestaurantSelectScreen(),
|
||||
servicePointSelect: (_) => const ServicePointSelectScreen(),
|
||||
menuBrowse: (_) => const MenuBrowseScreen(),
|
||||
cartView: (_) => const CartViewScreen(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class AppState extends ChangeNotifier {
|
|||
|
||||
int? _cartOrderId;
|
||||
String? _cartOrderUuid;
|
||||
int _cartItemCount = 0;
|
||||
|
||||
int? get selectedBusinessId => _selectedBusinessId;
|
||||
int? get selectedServicePointId => _selectedServicePointId;
|
||||
|
|
@ -17,6 +18,7 @@ class AppState extends ChangeNotifier {
|
|||
|
||||
int? get cartOrderId => _cartOrderId;
|
||||
String? get cartOrderUuid => _cartOrderUuid;
|
||||
int get cartItemCount => _cartItemCount;
|
||||
|
||||
bool get hasLocationSelection =>
|
||||
_selectedBusinessId != null && _selectedServicePointId != null;
|
||||
|
|
@ -50,15 +52,22 @@ class AppState extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCartOrder({required int orderId, required String orderUuid}) {
|
||||
void setCartOrder({required int orderId, required String orderUuid, int itemCount = 0}) {
|
||||
_cartOrderId = orderId;
|
||||
_cartOrderUuid = orderUuid;
|
||||
_cartItemCount = itemCount;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateCartItemCount(int count) {
|
||||
_cartItemCount = count;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearCart() {
|
||||
_cartOrderId = null;
|
||||
_cartOrderUuid = null;
|
||||
_cartItemCount = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
|
|
|||
202
lib/models/cart.dart
Normal file
202
lib/models/cart.dart
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
class Cart {
|
||||
final int orderId;
|
||||
final String orderUuid;
|
||||
final int userId;
|
||||
final int businessId;
|
||||
final double businessDeliveryMultiplier;
|
||||
final int orderTypeId;
|
||||
final double deliveryFee;
|
||||
final int statusId;
|
||||
final int? addressId;
|
||||
final int? paymentId;
|
||||
final String? remarks;
|
||||
final DateTime addedOn;
|
||||
final DateTime lastEditedOn;
|
||||
final DateTime? submittedOn;
|
||||
final int servicePointId;
|
||||
final List<OrderLineItem> lineItems;
|
||||
|
||||
const Cart({
|
||||
required this.orderId,
|
||||
required this.orderUuid,
|
||||
required this.userId,
|
||||
required this.businessId,
|
||||
required this.businessDeliveryMultiplier,
|
||||
required this.orderTypeId,
|
||||
required this.deliveryFee,
|
||||
required this.statusId,
|
||||
this.addressId,
|
||||
this.paymentId,
|
||||
this.remarks,
|
||||
required this.addedOn,
|
||||
required this.lastEditedOn,
|
||||
this.submittedOn,
|
||||
required this.servicePointId,
|
||||
required this.lineItems,
|
||||
});
|
||||
|
||||
factory Cart.fromJson(Map<String, dynamic> json) {
|
||||
final order = (json["ORDER"] ?? json["Order"]) as Map<String, dynamic>? ?? {};
|
||||
final lineItemsJson = (json["ORDERLINEITEMS"] ?? json["OrderLineItems"]) as List? ?? [];
|
||||
|
||||
return Cart(
|
||||
orderId: _parseInt(order["OrderID"]) ?? 0,
|
||||
orderUuid: (order["OrderUUID"] as String?) ?? "",
|
||||
userId: _parseInt(order["OrderUserID"]) ?? 0,
|
||||
businessId: _parseInt(order["OrderBusinessID"]) ?? 0,
|
||||
businessDeliveryMultiplier: _parseDouble(order["OrderBusinessDeliveryMultiplier"]) ?? 0.0,
|
||||
orderTypeId: _parseInt(order["OrderTypeID"]) ?? 0,
|
||||
deliveryFee: _parseDouble(order["OrderDeliveryFee"]) ?? 0.0,
|
||||
statusId: _parseInt(order["OrderStatusID"]) ?? 0,
|
||||
addressId: _parseInt(order["OrderAddressID"]),
|
||||
paymentId: _parseInt(order["OrderPaymentID"]),
|
||||
remarks: order["OrderRemarks"] as String?,
|
||||
addedOn: _parseDateTime(order["OrderAddedOn"]),
|
||||
lastEditedOn: _parseDateTime(order["OrderLastEditedOn"]),
|
||||
submittedOn: _parseDateTime(order["OrderSubmittedOn"]),
|
||||
servicePointId: _parseInt(order["OrderServicePointID"]) ?? 0,
|
||||
lineItems: lineItemsJson
|
||||
.map((item) => OrderLineItem.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
static int? _parseInt(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) {
|
||||
if (value.isEmpty) return null;
|
||||
return int.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static double? _parseDouble(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is double) return value;
|
||||
if (value is num) return value.toDouble();
|
||||
if (value is String) {
|
||||
if (value.isEmpty) return null;
|
||||
return double.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static DateTime _parseDateTime(dynamic value) {
|
||||
if (value == null) return DateTime.now();
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
return DateTime.now();
|
||||
}
|
||||
}
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
double get subtotal {
|
||||
return lineItems
|
||||
.where((item) => !item.isDeleted && item.parentOrderLineItemId == 0)
|
||||
.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
|
||||
}
|
||||
|
||||
double get total => subtotal + deliveryFee;
|
||||
|
||||
int get itemCount {
|
||||
return lineItems
|
||||
.where((item) => !item.isDeleted && item.parentOrderLineItemId == 0)
|
||||
.fold(0, (sum, item) => sum + item.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
class OrderLineItem {
|
||||
final int orderLineItemId;
|
||||
final int parentOrderLineItemId;
|
||||
final int orderId;
|
||||
final int itemId;
|
||||
final int statusId;
|
||||
final double price;
|
||||
final int quantity;
|
||||
final String? remark;
|
||||
final bool isDeleted;
|
||||
final DateTime addedOn;
|
||||
|
||||
const OrderLineItem({
|
||||
required this.orderLineItemId,
|
||||
required this.parentOrderLineItemId,
|
||||
required this.orderId,
|
||||
required this.itemId,
|
||||
required this.statusId,
|
||||
required this.price,
|
||||
required this.quantity,
|
||||
this.remark,
|
||||
required this.isDeleted,
|
||||
required this.addedOn,
|
||||
});
|
||||
|
||||
factory OrderLineItem.fromJson(Map<String, dynamic> json) {
|
||||
return OrderLineItem(
|
||||
orderLineItemId: _parseInt(json["OrderLineItemID"]) ?? 0,
|
||||
parentOrderLineItemId: _parseInt(json["OrderLineItemParentOrderLineItemID"]) ?? 0,
|
||||
orderId: _parseInt(json["OrderLineItemOrderID"]) ?? 0,
|
||||
itemId: _parseInt(json["OrderLineItemItemID"]) ?? 0,
|
||||
statusId: _parseInt(json["OrderLineItemStatusID"]) ?? 0,
|
||||
price: _parseDouble(json["OrderLineItemPrice"]) ?? 0.0,
|
||||
quantity: _parseInt(json["OrderLineItemQuantity"]) ?? 0,
|
||||
remark: json["OrderLineItemRemark"] as String?,
|
||||
isDeleted: _parseBool(json["OrderLineItemIsDeleted"]),
|
||||
addedOn: _parseDateTime(json["OrderLineItemAddedOn"]),
|
||||
);
|
||||
}
|
||||
|
||||
static int? _parseInt(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) {
|
||||
if (value.isEmpty) return null;
|
||||
return int.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static double? _parseDouble(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is double) return value;
|
||||
if (value is num) return value.toDouble();
|
||||
if (value is String) {
|
||||
if (value.isEmpty) return null;
|
||||
return double.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _parseBool(dynamic value) {
|
||||
if (value == null) return false;
|
||||
if (value is bool) return value;
|
||||
if (value is num) return value != 0;
|
||||
if (value is String) {
|
||||
final lower = value.toLowerCase();
|
||||
return lower == "true" || lower == "1";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static DateTime _parseDateTime(dynamic value) {
|
||||
if (value == null) return DateTime.now();
|
||||
if (value is DateTime) return value;
|
||||
if (value is String) {
|
||||
try {
|
||||
return DateTime.parse(value);
|
||||
} catch (e) {
|
||||
return DateTime.now();
|
||||
}
|
||||
}
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
bool get isRootItem => parentOrderLineItemId == 0;
|
||||
bool get isModifier => parentOrderLineItemId != 0;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
class MenuItem {
|
||||
final int itemId;
|
||||
final int categoryId;
|
||||
final String categoryName;
|
||||
final String name;
|
||||
final String description;
|
||||
final int parentItemId;
|
||||
|
|
@ -15,6 +16,7 @@ class MenuItem {
|
|||
const MenuItem({
|
||||
required this.itemId,
|
||||
required this.categoryId,
|
||||
required this.categoryName,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.parentItemId,
|
||||
|
|
@ -31,6 +33,7 @@ class MenuItem {
|
|||
return MenuItem(
|
||||
itemId: (json["ItemID"] as num).toInt(),
|
||||
categoryId: (json["ItemCategoryID"] as num).toInt(),
|
||||
categoryName: (json["ItemCategoryName"] as String?) ?? "",
|
||||
name: (json["ItemName"] as String?) ?? "",
|
||||
description: (json["ItemDescription"] as String?) ?? "",
|
||||
parentItemId: (json["ItemParentItemID"] as num?)?.toInt() ?? 0,
|
||||
|
|
|
|||
462
lib/screens/cart_view_screen.dart
Normal file
462
lib/screens/cart_view_screen.dart
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../app/app_state.dart';
|
||||
import '../models/cart.dart';
|
||||
import '../models/menu_item.dart';
|
||||
import '../services/api.dart';
|
||||
|
||||
class CartViewScreen extends StatefulWidget {
|
||||
const CartViewScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CartViewScreen> createState() => _CartViewScreenState();
|
||||
}
|
||||
|
||||
class _CartViewScreenState extends State<CartViewScreen> {
|
||||
Cart? _cart;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
Map<int, MenuItem> _menuItemsById = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCart();
|
||||
}
|
||||
|
||||
Future<void> _loadCart() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final appState = context.read<AppState>();
|
||||
final cartOrderId = appState.cartOrderId;
|
||||
|
||||
if (cartOrderId == null) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_cart = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Load cart
|
||||
final cart = await Api.getCart(orderId: cartOrderId);
|
||||
|
||||
// Load menu items to get names and prices
|
||||
final businessId = appState.selectedBusinessId;
|
||||
if (businessId != null) {
|
||||
final menuItems = await Api.listMenuItems(businessId: businessId);
|
||||
_menuItemsById = {for (var item in menuItems) item.itemId: item};
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_cart = cart;
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Update item count in app state
|
||||
appState.updateCartItemCount(cart.itemCount);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removeLineItem(OrderLineItem lineItem) async {
|
||||
try {
|
||||
final appState = context.read<AppState>();
|
||||
final cartOrderId = appState.cartOrderId;
|
||||
if (cartOrderId == null) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Set IsSelected=false to remove the item
|
||||
await Api.setLineItem(
|
||||
orderId: cartOrderId,
|
||||
parentOrderLineItemId: lineItem.parentOrderLineItemId,
|
||||
itemId: lineItem.itemId,
|
||||
isSelected: false,
|
||||
);
|
||||
|
||||
// Reload cart
|
||||
await _loadCart();
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateQuantity(OrderLineItem lineItem, int newQuantity) async {
|
||||
if (newQuantity < 1) return;
|
||||
|
||||
try {
|
||||
final appState = context.read<AppState>();
|
||||
final cartOrderId = appState.cartOrderId;
|
||||
if (cartOrderId == null) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
await Api.setLineItem(
|
||||
orderId: cartOrderId,
|
||||
parentOrderLineItemId: lineItem.parentOrderLineItemId,
|
||||
itemId: lineItem.itemId,
|
||||
isSelected: true,
|
||||
quantity: newQuantity,
|
||||
remark: lineItem.remark,
|
||||
);
|
||||
|
||||
// Reload cart
|
||||
await _loadCart();
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submitOrder() async {
|
||||
try {
|
||||
final appState = context.read<AppState>();
|
||||
final cartOrderId = appState.cartOrderId;
|
||||
if (cartOrderId == null) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
await Api.submitOrder(orderId: cartOrderId);
|
||||
|
||||
// Clear cart state
|
||||
appState.clearCart();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Order submitted successfully!"),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Cart"),
|
||||
backgroundColor: Colors.black,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _error != null
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error, color: Colors.red, size: 48),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Error loading cart",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(_error!, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadCart,
|
||||
child: const Text("Retry"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: _cart == null || _cart!.itemCount == 0
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shopping_cart_outlined,
|
||||
size: 80,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Your cart is empty",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: _buildCartItems(),
|
||||
),
|
||||
),
|
||||
_buildCartSummary(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildCartItems() {
|
||||
if (_cart == null) return [];
|
||||
|
||||
// Group line items by root items
|
||||
final rootItems = _cart!.lineItems
|
||||
.where((item) => item.parentOrderLineItemId == 0 && !item.isDeleted)
|
||||
.toList();
|
||||
|
||||
final widgets = <Widget>[];
|
||||
|
||||
for (final rootItem in rootItems) {
|
||||
widgets.add(_buildRootItemCard(rootItem));
|
||||
widgets.add(const SizedBox(height: 12));
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
Widget _buildRootItemCard(OrderLineItem rootItem) {
|
||||
final menuItem = _menuItemsById[rootItem.itemId];
|
||||
final itemName = menuItem?.name ?? "Item #${rootItem.itemId}";
|
||||
|
||||
// Find all modifiers for this root item
|
||||
final modifiers = _cart!.lineItems
|
||||
.where((item) =>
|
||||
item.parentOrderLineItemId == rootItem.orderLineItemId &&
|
||||
!item.isDeleted)
|
||||
.toList();
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
itemName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _confirmRemoveItem(rootItem, itemName),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (modifiers.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
...modifiers.map((mod) => _buildModifierRow(mod)),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
onPressed: rootItem.quantity > 1
|
||||
? () => _updateQuantity(rootItem, rootItem.quantity - 1)
|
||||
: null,
|
||||
),
|
||||
Text(
|
||||
"${rootItem.quantity}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: () =>
|
||||
_updateQuantity(rootItem, rootItem.quantity + 1),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
"\$${rootItem.price.toStringAsFixed(2)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModifierRow(OrderLineItem modifier) {
|
||||
final menuItem = _menuItemsById[modifier.itemId];
|
||||
final modName = menuItem?.name ?? "Modifier #${modifier.itemId}";
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.add, size: 12, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
modName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modifier.price > 0)
|
||||
Text(
|
||||
"+\$${modifier.price.toStringAsFixed(2)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCartSummary() {
|
||||
if (_cart == null) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Subtotal",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
"\$${_cart!.subtotal.toStringAsFixed(2)}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_cart!.deliveryFee > 0) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Delivery Fee",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
"\$${_cart!.deliveryFee.toStringAsFixed(2)}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Total",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"\$${_cart!.total.toStringAsFixed(2)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _cart!.itemCount > 0 ? _submitOrder : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.black,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: const Text(
|
||||
"Submit Order",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmRemoveItem(OrderLineItem item, String itemName) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("Remove Item"),
|
||||
content: Text("Remove $itemName from cart?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_removeLineItem(item);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text("Remove"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
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 "../models/menu_item.dart";
|
||||
import "../services/api.dart";
|
||||
|
||||
|
|
@ -98,16 +102,20 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<AppState>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Menu"),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.shopping_cart),
|
||||
icon: Badge(
|
||||
label: Text("${appState.cartItemCount}"),
|
||||
isLabelVisible: appState.cartItemCount > 0,
|
||||
child: const Icon(Icons.shopping_cart),
|
||||
),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Cart view coming soon")),
|
||||
);
|
||||
Navigator.of(context).pushNamed(AppRoutes.cartView);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -154,6 +162,9 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
|||
itemBuilder: (context, index) {
|
||||
final categoryId = categoryIds[index];
|
||||
final items = _itemsByCategory[categoryId] ?? [];
|
||||
final categoryName = items.isNotEmpty
|
||||
? items.first.categoryName
|
||||
: "Category $categoryId";
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -161,7 +172,7 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
"Category $categoryId",
|
||||
categoryName,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
|
@ -222,14 +233,106 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
void _addToCart(MenuItem item, Set<int> selectedModifierIds) {
|
||||
final message = selectedModifierIds.isEmpty
|
||||
? "Added ${item.name} to cart"
|
||||
: "Added ${item.name} with ${selectedModifierIds.length} customizations";
|
||||
Future<void> _addToCart(MenuItem item, Set<int> selectedModifierIds) async {
|
||||
if (_userId == null || _businessId == null || _servicePointId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Missing required information")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
try {
|
||||
final appState = context.read<AppState>();
|
||||
|
||||
// Get or create cart
|
||||
Cart cart;
|
||||
if (appState.cartOrderId == null) {
|
||||
cart = await Api.getOrCreateCart(
|
||||
userId: _userId!,
|
||||
businessId: _businessId!,
|
||||
servicePointId: _servicePointId!,
|
||||
orderTypeId: 1, // Dine-in
|
||||
);
|
||||
// ignore: avoid_print
|
||||
print("DEBUG: Created cart with orderId=${cart.orderId}");
|
||||
appState.setCartOrder(
|
||||
orderId: cart.orderId,
|
||||
orderUuid: cart.orderUuid,
|
||||
itemCount: cart.itemCount,
|
||||
);
|
||||
} else {
|
||||
// We have an existing cart ID
|
||||
cart = await Api.getCart(orderId: appState.cartOrderId!);
|
||||
// ignore: avoid_print
|
||||
print("DEBUG: Loaded existing cart with orderId=${cart.orderId}");
|
||||
}
|
||||
|
||||
// ignore: avoid_print
|
||||
print("DEBUG: About to add root item ${item.itemId} to cart ${cart.orderId}");
|
||||
|
||||
// Add root item
|
||||
cart = await Api.setLineItem(
|
||||
orderId: cart.orderId,
|
||||
parentOrderLineItemId: 0,
|
||||
itemId: item.itemId,
|
||||
isSelected: true,
|
||||
quantity: 1,
|
||||
);
|
||||
|
||||
// ignore: avoid_print
|
||||
print("DEBUG: Added root item, cart now has ${cart.lineItems.length} line items");
|
||||
|
||||
// Add all selected modifiers recursively
|
||||
await _addModifiersRecursively(
|
||||
cart.orderId,
|
||||
item.itemId,
|
||||
selectedModifierIds,
|
||||
);
|
||||
|
||||
// Refresh cart to get final state
|
||||
cart = await Api.getCart(orderId: cart.orderId);
|
||||
appState.updateCartItemCount(cart.itemCount);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final message = selectedModifierIds.isEmpty
|
||||
? "Added ${item.name} to cart"
|
||||
: "Added ${item.name} with ${selectedModifierIds.length} customizations";
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error adding to cart: $e")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addModifiersRecursively(
|
||||
int orderId,
|
||||
int parentItemId,
|
||||
Set<int> selectedItemIds,
|
||||
) async {
|
||||
final children = _itemsByParent[parentItemId] ?? [];
|
||||
|
||||
for (final child in children) {
|
||||
final isSelected = selectedItemIds.contains(child.itemId);
|
||||
|
||||
await Api.setLineItem(
|
||||
orderId: orderId,
|
||||
parentOrderLineItemId: 0, // Will be handled by backend
|
||||
itemId: child.itemId,
|
||||
isSelected: isSelected,
|
||||
);
|
||||
|
||||
// Recursively add grandchildren
|
||||
if (isSelected) {
|
||||
await _addModifiersRecursively(orderId, child.itemId, selectedItemIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import "dart:convert";
|
||||
import "package:http/http.dart" as http;
|
||||
|
||||
import "../models/cart.dart";
|
||||
import "../models/menu_item.dart";
|
||||
import "../models/restaurant.dart";
|
||||
import "../models/service_point.dart";
|
||||
|
|
@ -307,33 +308,97 @@ class Api {
|
|||
}
|
||||
|
||||
// -------------------------
|
||||
// Ordering API (stubs referenced by OrderHomeScreen)
|
||||
// Cart & Orders
|
||||
// -------------------------
|
||||
|
||||
static Future<dynamic> getOrCreateCart({
|
||||
static Future<Cart> getOrCreateCart({
|
||||
required int userId,
|
||||
required int businessId,
|
||||
required int servicePointId,
|
||||
required int orderTypeId,
|
||||
}) async {
|
||||
throw StateError("endpoint_not_implemented: Api.getOrCreateCart");
|
||||
final raw = await _postRaw(
|
||||
"/orders/getOrCreateCart.cfm",
|
||||
{
|
||||
"OrderUserID": userId,
|
||||
"BusinessID": businessId,
|
||||
"OrderServicePointID": servicePointId,
|
||||
"OrderTypeID": orderTypeId,
|
||||
},
|
||||
businessIdOverride: businessId,
|
||||
);
|
||||
|
||||
final j = _requireJson(raw, "GetOrCreateCart");
|
||||
|
||||
if (!_ok(j)) {
|
||||
throw StateError(
|
||||
"GetOrCreateCart API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
return Cart.fromJson(j);
|
||||
}
|
||||
|
||||
static Future<dynamic> getCart({required int orderId}) async {
|
||||
throw StateError("endpoint_not_implemented: Api.getCart");
|
||||
static Future<Cart> getCart({required int orderId}) async {
|
||||
final raw = await _postRaw(
|
||||
"/orders/getCart.cfm",
|
||||
{"OrderID": orderId},
|
||||
);
|
||||
|
||||
final j = _requireJson(raw, "GetCart");
|
||||
|
||||
if (!_ok(j)) {
|
||||
throw StateError(
|
||||
"GetCart API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
return Cart.fromJson(j);
|
||||
}
|
||||
|
||||
static Future<void> setLineItem({
|
||||
static Future<Cart> setLineItem({
|
||||
required int orderId,
|
||||
required int parentOrderLineItemId,
|
||||
required int itemId,
|
||||
required int qty,
|
||||
required List<int> selectedChildItemIds,
|
||||
required bool isSelected,
|
||||
int quantity = 1,
|
||||
String? remark,
|
||||
}) async {
|
||||
throw StateError("endpoint_not_implemented: Api.setLineItem");
|
||||
final raw = await _postRaw(
|
||||
"/orders/setLineItem.cfm",
|
||||
{
|
||||
"OrderID": orderId,
|
||||
"ParentOrderLineItemID": parentOrderLineItemId,
|
||||
"ItemID": itemId,
|
||||
"IsSelected": isSelected,
|
||||
"Quantity": quantity,
|
||||
if (remark != null && remark.isNotEmpty) "Remark": remark,
|
||||
},
|
||||
);
|
||||
|
||||
final j = _requireJson(raw, "SetLineItem");
|
||||
|
||||
if (!_ok(j)) {
|
||||
throw StateError(
|
||||
"SetLineItem API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}",
|
||||
);
|
||||
}
|
||||
|
||||
return Cart.fromJson(j);
|
||||
}
|
||||
|
||||
static Future<void> submitOrder({required int orderId}) async {
|
||||
throw StateError("endpoint_not_implemented: Api.submitOrder");
|
||||
final raw = await _postRaw(
|
||||
"/orders/submit.cfm",
|
||||
{"OrderID": orderId},
|
||||
);
|
||||
|
||||
final j = _requireJson(raw, "SubmitOrder");
|
||||
|
||||
if (!_ok(j)) {
|
||||
throw StateError(
|
||||
"SubmitOrder API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue