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 "package:flutter/material.dart";
|
||||||
|
|
||||||
|
import "../screens/cart_view_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/restaurant_select_screen.dart";
|
import "../screens/restaurant_select_screen.dart";
|
||||||
|
|
@ -12,6 +13,7 @@ class AppRoutes {
|
||||||
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";
|
||||||
|
static const String cartView = "/cart";
|
||||||
|
|
||||||
static Map<String, WidgetBuilder> get routes => {
|
static Map<String, WidgetBuilder> get routes => {
|
||||||
splash: (_) => const SplashScreen(),
|
splash: (_) => const SplashScreen(),
|
||||||
|
|
@ -19,5 +21,6 @@ class AppRoutes {
|
||||||
restaurantSelect: (_) => const RestaurantSelectScreen(),
|
restaurantSelect: (_) => const RestaurantSelectScreen(),
|
||||||
servicePointSelect: (_) => const ServicePointSelectScreen(),
|
servicePointSelect: (_) => const ServicePointSelectScreen(),
|
||||||
menuBrowse: (_) => const MenuBrowseScreen(),
|
menuBrowse: (_) => const MenuBrowseScreen(),
|
||||||
|
cartView: (_) => const CartViewScreen(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ class AppState extends ChangeNotifier {
|
||||||
|
|
||||||
int? _cartOrderId;
|
int? _cartOrderId;
|
||||||
String? _cartOrderUuid;
|
String? _cartOrderUuid;
|
||||||
|
int _cartItemCount = 0;
|
||||||
|
|
||||||
int? get selectedBusinessId => _selectedBusinessId;
|
int? get selectedBusinessId => _selectedBusinessId;
|
||||||
int? get selectedServicePointId => _selectedServicePointId;
|
int? get selectedServicePointId => _selectedServicePointId;
|
||||||
|
|
@ -17,6 +18,7 @@ class AppState extends ChangeNotifier {
|
||||||
|
|
||||||
int? get cartOrderId => _cartOrderId;
|
int? get cartOrderId => _cartOrderId;
|
||||||
String? get cartOrderUuid => _cartOrderUuid;
|
String? get cartOrderUuid => _cartOrderUuid;
|
||||||
|
int get cartItemCount => _cartItemCount;
|
||||||
|
|
||||||
bool get hasLocationSelection =>
|
bool get hasLocationSelection =>
|
||||||
_selectedBusinessId != null && _selectedServicePointId != null;
|
_selectedBusinessId != null && _selectedServicePointId != null;
|
||||||
|
|
@ -50,15 +52,22 @@ class AppState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCartOrder({required int orderId, required String orderUuid}) {
|
void setCartOrder({required int orderId, required String orderUuid, int itemCount = 0}) {
|
||||||
_cartOrderId = orderId;
|
_cartOrderId = orderId;
|
||||||
_cartOrderUuid = orderUuid;
|
_cartOrderUuid = orderUuid;
|
||||||
|
_cartItemCount = itemCount;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCartItemCount(int count) {
|
||||||
|
_cartItemCount = count;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearCart() {
|
void clearCart() {
|
||||||
_cartOrderId = null;
|
_cartOrderId = null;
|
||||||
_cartOrderUuid = null;
|
_cartOrderUuid = null;
|
||||||
|
_cartItemCount = 0;
|
||||||
notifyListeners();
|
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 {
|
class MenuItem {
|
||||||
final int itemId;
|
final int itemId;
|
||||||
final int categoryId;
|
final int categoryId;
|
||||||
|
final String categoryName;
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
final int parentItemId;
|
final int parentItemId;
|
||||||
|
|
@ -15,6 +16,7 @@ class MenuItem {
|
||||||
const MenuItem({
|
const MenuItem({
|
||||||
required this.itemId,
|
required this.itemId,
|
||||||
required this.categoryId,
|
required this.categoryId,
|
||||||
|
required this.categoryName,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.parentItemId,
|
required this.parentItemId,
|
||||||
|
|
@ -31,6 +33,7 @@ class MenuItem {
|
||||||
return MenuItem(
|
return MenuItem(
|
||||||
itemId: (json["ItemID"] as num).toInt(),
|
itemId: (json["ItemID"] as num).toInt(),
|
||||||
categoryId: (json["ItemCategoryID"] as num).toInt(),
|
categoryId: (json["ItemCategoryID"] as num).toInt(),
|
||||||
|
categoryName: (json["ItemCategoryName"] as String?) ?? "",
|
||||||
name: (json["ItemName"] as String?) ?? "",
|
name: (json["ItemName"] as String?) ?? "",
|
||||||
description: (json["ItemDescription"] as String?) ?? "",
|
description: (json["ItemDescription"] as String?) ?? "",
|
||||||
parentItemId: (json["ItemParentItemID"] as num?)?.toInt() ?? 0,
|
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: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 "../models/menu_item.dart";
|
||||||
import "../services/api.dart";
|
import "../services/api.dart";
|
||||||
|
|
||||||
|
|
@ -98,16 +102,20 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final appState = context.watch<AppState>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Menu"),
|
title: const Text("Menu"),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.shopping_cart),
|
icon: Badge(
|
||||||
|
label: Text("${appState.cartItemCount}"),
|
||||||
|
isLabelVisible: appState.cartItemCount > 0,
|
||||||
|
child: const Icon(Icons.shopping_cart),
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
Navigator.of(context).pushNamed(AppRoutes.cartView);
|
||||||
const SnackBar(content: Text("Cart view coming soon")),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -154,6 +162,9 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final categoryId = categoryIds[index];
|
final categoryId = categoryIds[index];
|
||||||
final items = _itemsByCategory[categoryId] ?? [];
|
final items = _itemsByCategory[categoryId] ?? [];
|
||||||
|
final categoryName = items.isNotEmpty
|
||||||
|
? items.first.categoryName
|
||||||
|
: "Category $categoryId";
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -161,7 +172,7 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Category $categoryId",
|
categoryName,
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
|
@ -222,7 +233,68 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addToCart(MenuItem item, Set<int> selectedModifierIds) {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
final message = selectedModifierIds.isEmpty
|
||||||
? "Added ${item.name} to cart"
|
? "Added ${item.name} to cart"
|
||||||
: "Added ${item.name} with ${selectedModifierIds.length} customizations";
|
: "Added ${item.name} with ${selectedModifierIds.length} customizations";
|
||||||
|
|
@ -230,6 +302,37 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(message)),
|
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 "dart:convert";
|
||||||
import "package:http/http.dart" as http;
|
import "package:http/http.dart" as http;
|
||||||
|
|
||||||
|
import "../models/cart.dart";
|
||||||
import "../models/menu_item.dart";
|
import "../models/menu_item.dart";
|
||||||
import "../models/restaurant.dart";
|
import "../models/restaurant.dart";
|
||||||
import "../models/service_point.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 userId,
|
||||||
required int businessId,
|
required int businessId,
|
||||||
required int servicePointId,
|
required int servicePointId,
|
||||||
required int orderTypeId,
|
required int orderTypeId,
|
||||||
}) async {
|
}) 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}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<dynamic> getCart({required int orderId}) async {
|
return Cart.fromJson(j);
|
||||||
throw StateError("endpoint_not_implemented: Api.getCart");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setLineItem({
|
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<Cart> setLineItem({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
required int parentOrderLineItemId,
|
required int parentOrderLineItemId,
|
||||||
required int itemId,
|
required int itemId,
|
||||||
required int qty,
|
required bool isSelected,
|
||||||
required List<int> selectedChildItemIds,
|
int quantity = 1,
|
||||||
|
String? remark,
|
||||||
}) async {
|
}) 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 {
|
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