- Fix login to check for existing cart after OTP verification - Add abandonOrder API call for Start Fresh functionality - Fix stale service point showing for non-dine-in orders - Add Chat button for non-dine-in orders (was only Call Server) - Add quantity selector in item customization sheet - Compact cart layout with quantity badge, accordion modifiers - Add scroll indicator when cart has hidden content - Fix restaurant list tappable before images load - Add ForceNew parameter to setLineItem for customized items Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
329 lines
11 KiB
Dart
329 lines
11 KiB
Dart
class Cart {
|
|
final int orderId;
|
|
final String orderUuid;
|
|
final int userId;
|
|
final int businessId;
|
|
final double businessDeliveryMultiplier;
|
|
final double businessDeliveryFee; // The business's standard delivery fee (for preview)
|
|
final List<int> businessOrderTypes; // Which order types the business offers (1=dine-in, 2=takeaway, 3=delivery)
|
|
final int orderTypeId;
|
|
final double deliveryFee; // The actual delivery fee on this order (set when order type is confirmed)
|
|
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.businessDeliveryFee,
|
|
required this.businessOrderTypes,
|
|
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,
|
|
});
|
|
|
|
// Helper methods for checking available order types
|
|
bool get offersDineIn => businessOrderTypes.contains(1);
|
|
bool get offersTakeaway => businessOrderTypes.contains(2);
|
|
bool get offersDelivery => businessOrderTypes.contains(3);
|
|
|
|
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? ?? [];
|
|
|
|
// Parse business order types - can be array of ints or strings
|
|
List<int> orderTypes = [1, 2, 3]; // Default to all types
|
|
final rawOrderTypes = order["BusinessOrderTypes"];
|
|
if (rawOrderTypes != null && rawOrderTypes is List) {
|
|
orderTypes = rawOrderTypes
|
|
.map((e) => e is int ? e : int.tryParse(e.toString()) ?? 0)
|
|
.where((e) => e > 0)
|
|
.toList();
|
|
if (orderTypes.isEmpty) orderTypes = [1, 2, 3];
|
|
}
|
|
|
|
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,
|
|
businessDeliveryFee: _parseDouble(order["BusinessDeliveryFee"]) ?? 0.0,
|
|
businessOrderTypes: orderTypes,
|
|
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 {
|
|
// Sum all non-deleted line items (root items and modifiers)
|
|
// Root items have their base price, modifiers have their add-on prices
|
|
// Modifier prices are multiplied by the root item's quantity
|
|
double total = 0.0;
|
|
|
|
// First, get all root items and their prices
|
|
final rootItems = lineItems.where((item) => !item.isDeleted && item.parentOrderLineItemId == 0);
|
|
for (final rootItem in rootItems) {
|
|
total += rootItem.price * rootItem.quantity;
|
|
|
|
// Add all modifier prices for this root item (recursively)
|
|
total += _sumModifierPrices(rootItem.orderLineItemId, rootItem.quantity);
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
/// Recursively sum modifier prices for a parent line item
|
|
double _sumModifierPrices(int parentOrderLineItemId, int rootQuantity) {
|
|
double total = 0.0;
|
|
final children = lineItems.where(
|
|
(item) => !item.isDeleted && item.parentOrderLineItemId == parentOrderLineItemId
|
|
);
|
|
|
|
for (final child in children) {
|
|
// Modifier price is multiplied by root item quantity
|
|
total += child.price * rootQuantity;
|
|
// Recursively add grandchildren modifier prices
|
|
total += _sumModifierPrices(child.orderLineItemId, rootQuantity);
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
// Only include delivery fee for delivery orders (orderTypeId == 3)
|
|
// Sales tax rate (8.25% for California - can be made configurable per business later)
|
|
static const double taxRate = 0.0825;
|
|
|
|
// Calculate sales tax on subtotal
|
|
double get tax => subtotal * taxRate;
|
|
|
|
double get total => subtotal + tax + (orderTypeId == 3 ? deliveryFee : 0);
|
|
|
|
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;
|
|
final String? itemName;
|
|
final int? itemParentItemId;
|
|
final String? itemParentName;
|
|
final bool isCheckedByDefault;
|
|
|
|
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,
|
|
this.itemName,
|
|
this.itemParentItemId,
|
|
this.itemParentName,
|
|
this.isCheckedByDefault = false,
|
|
});
|
|
|
|
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"]),
|
|
itemName: json["ItemName"] as String?,
|
|
itemParentItemId: _parseInt(json["ItemParentItemID"]),
|
|
itemParentName: json["ItemParentName"] as String?,
|
|
isCheckedByDefault: _parseBool(json["ItemIsCheckedByDefault"]),
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// Lightweight cart info returned by getActiveCart API
|
|
/// Used at app startup to check if user has an existing order
|
|
class ActiveCartInfo {
|
|
final int orderId;
|
|
final String orderUuid;
|
|
final int businessId;
|
|
final String businessName;
|
|
final int orderTypeId;
|
|
final String orderTypeName;
|
|
final int servicePointId;
|
|
final String servicePointName;
|
|
final int itemCount;
|
|
|
|
const ActiveCartInfo({
|
|
required this.orderId,
|
|
required this.orderUuid,
|
|
required this.businessId,
|
|
required this.businessName,
|
|
required this.orderTypeId,
|
|
required this.orderTypeName,
|
|
required this.servicePointId,
|
|
required this.servicePointName,
|
|
required this.itemCount,
|
|
});
|
|
|
|
factory ActiveCartInfo.fromJson(Map<String, dynamic> json) {
|
|
return ActiveCartInfo(
|
|
orderId: _parseInt(json["OrderID"]) ?? 0,
|
|
orderUuid: json["OrderUUID"]?.toString() ?? "",
|
|
businessId: _parseInt(json["BusinessID"]) ?? 0,
|
|
businessName: json["BusinessName"]?.toString() ?? "",
|
|
orderTypeId: _parseInt(json["OrderTypeID"]) ?? 0,
|
|
orderTypeName: json["OrderTypeName"]?.toString() ?? "",
|
|
servicePointId: _parseInt(json["ServicePointID"]) ?? 0,
|
|
servicePointName: json["ServicePointName"]?.toString() ?? "",
|
|
itemCount: _parseInt(json["ItemCount"]) ?? 0,
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool get hasItems => itemCount > 0;
|
|
bool get isDineIn => orderTypeId == 1;
|
|
bool get isTakeaway => orderTypeId == 2;
|
|
bool get isDelivery => orderTypeId == 3;
|
|
bool get isUndecided => orderTypeId == 0;
|
|
}
|