From 77e314517511771d502ead0c10d42385057b118a Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Fri, 9 Jan 2026 12:41:44 -0800 Subject: [PATCH] Add About screen and Order Detail screen with modifiers - Add About Payfrit screen with app info, features, and contact details - Add Order Detail screen showing line items with non-default modifiers - Add order_detail model with parent-child line item hierarchy - Update order history to navigate to detail screen on tap - Add getOrderDetail API method - Add about route to app router Co-Authored-By: Claude Opus 4.5 --- lib/app/app_router.dart | 3 + lib/models/order_detail.dart | 225 +++++++++++ lib/screens/about_screen.dart | 182 +++++++++ lib/screens/account_screen.dart | 11 + lib/screens/order_detail_screen.dart | 514 ++++++++++++++++++++++++++ lib/screens/order_history_screen.dart | 9 +- lib/services/api.dart | 14 + 7 files changed, 955 insertions(+), 3 deletions(-) create mode 100644 lib/models/order_detail.dart create mode 100644 lib/screens/about_screen.dart create mode 100644 lib/screens/order_detail_screen.dart diff --git a/lib/app/app_router.dart b/lib/app/app_router.dart index f9f1de8..0021fc4 100644 --- a/lib/app/app_router.dart +++ b/lib/app/app_router.dart @@ -1,6 +1,7 @@ import "package:flutter/material.dart"; import "../screens/account_screen.dart"; +import "../screens/about_screen.dart"; import "../screens/address_edit_screen.dart"; import "../screens/address_list_screen.dart"; import "../screens/beacon_scan_screen.dart"; @@ -30,6 +31,7 @@ class AppRoutes { static const String profileSettings = "/profile-settings"; static const String addressList = "/addresses"; static const String addressEdit = "/address-edit"; + static const String about = "/about"; static Map get routes => { splash: (_) => const SplashScreen(), @@ -47,5 +49,6 @@ class AppRoutes { profileSettings: (_) => const ProfileSettingsScreen(), addressList: (_) => const AddressListScreen(), addressEdit: (_) => const AddressEditScreen(), + about: (_) => const AboutScreen(), }; } diff --git a/lib/models/order_detail.dart b/lib/models/order_detail.dart new file mode 100644 index 0000000..7f6233a --- /dev/null +++ b/lib/models/order_detail.dart @@ -0,0 +1,225 @@ +/// Model for order detail with line items and modifiers +class OrderDetail { + final int orderId; + final int businessId; + final String businessName; + final int status; + final String statusText; + final int orderTypeId; + final String orderTypeName; + final double subtotal; + final double tax; + final double tip; + final double total; + final String notes; + final DateTime createdOn; + final DateTime? submittedOn; + final DateTime? updatedOn; + final OrderCustomer customer; + final OrderServicePoint servicePoint; + final List lineItems; + + const OrderDetail({ + required this.orderId, + required this.businessId, + required this.businessName, + required this.status, + required this.statusText, + required this.orderTypeId, + required this.orderTypeName, + required this.subtotal, + required this.tax, + required this.tip, + required this.total, + required this.notes, + required this.createdOn, + this.submittedOn, + this.updatedOn, + required this.customer, + required this.servicePoint, + required this.lineItems, + }); + + factory OrderDetail.fromJson(Map json) { + final lineItemsJson = json['LineItems'] as List? ?? []; + + return OrderDetail( + orderId: _parseInt(json['OrderID']) ?? 0, + businessId: _parseInt(json['BusinessID']) ?? 0, + businessName: (json['BusinessName'] as String?) ?? '', + status: _parseInt(json['Status']) ?? 0, + statusText: (json['StatusText'] as String?) ?? '', + orderTypeId: _parseInt(json['OrderTypeID']) ?? 0, + orderTypeName: (json['OrderTypeName'] as String?) ?? '', + subtotal: _parseDouble(json['Subtotal']) ?? 0.0, + tax: _parseDouble(json['Tax']) ?? 0.0, + tip: _parseDouble(json['Tip']) ?? 0.0, + total: _parseDouble(json['Total']) ?? 0.0, + notes: (json['Notes'] as String?) ?? '', + createdOn: _parseDateTime(json['CreatedOn']), + submittedOn: _parseDateTimeNullable(json['SubmittedOn']), + updatedOn: _parseDateTimeNullable(json['UpdatedOn']), + customer: OrderCustomer.fromJson(json['Customer'] as Map? ?? {}), + servicePoint: OrderServicePoint.fromJson(json['ServicePoint'] as Map? ?? {}), + lineItems: lineItemsJson + .map((e) => OrderLineItemDetail.fromJson(e as Map)) + .toList(), + ); + } + + /// Get only non-default modifiers for display + List getNonDefaultModifiers(OrderLineItemDetail item) { + return item.modifiers.where((m) => !m.isDefault).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 && value.isNotEmpty) 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 && value.isNotEmpty) 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 && value.isNotEmpty) { + try { + return DateTime.parse(value); + } catch (_) {} + } + return DateTime.now(); + } + + static DateTime? _parseDateTimeNullable(dynamic value) { + if (value == null) return null; + if (value is String && value.isEmpty) return null; + if (value is DateTime) return value; + if (value is String) { + try { + return DateTime.parse(value); + } catch (_) {} + } + return null; + } +} + +class OrderCustomer { + final int userId; + final String firstName; + final String lastName; + final String phone; + final String email; + + const OrderCustomer({ + required this.userId, + required this.firstName, + required this.lastName, + required this.phone, + required this.email, + }); + + factory OrderCustomer.fromJson(Map json) { + return OrderCustomer( + userId: (json['UserID'] as num?)?.toInt() ?? 0, + firstName: (json['FirstName'] as String?) ?? '', + lastName: (json['LastName'] as String?) ?? '', + phone: (json['Phone'] as String?) ?? '', + email: (json['Email'] as String?) ?? '', + ); + } + + String get fullName { + final parts = [firstName, lastName].where((s) => s.isNotEmpty); + return parts.isEmpty ? 'Guest' : parts.join(' '); + } +} + +class OrderServicePoint { + final int servicePointId; + final String name; + final int typeId; + + const OrderServicePoint({ + required this.servicePointId, + required this.name, + required this.typeId, + }); + + factory OrderServicePoint.fromJson(Map json) { + return OrderServicePoint( + servicePointId: (json['ServicePointID'] as num?)?.toInt() ?? 0, + name: (json['Name'] as String?) ?? '', + typeId: (json['TypeID'] as num?)?.toInt() ?? 0, + ); + } +} + +class OrderLineItemDetail { + final int lineItemId; + final int itemId; + final int parentLineItemId; + final String itemName; + final int quantity; + final double unitPrice; + final String remarks; + final bool isDefault; + final List modifiers; + + const OrderLineItemDetail({ + required this.lineItemId, + required this.itemId, + required this.parentLineItemId, + required this.itemName, + required this.quantity, + required this.unitPrice, + required this.remarks, + required this.isDefault, + required this.modifiers, + }); + + factory OrderLineItemDetail.fromJson(Map json) { + final modifiersJson = json['Modifiers'] as List? ?? []; + + return OrderLineItemDetail( + lineItemId: (json['LineItemID'] as num?)?.toInt() ?? 0, + itemId: (json['ItemID'] as num?)?.toInt() ?? 0, + parentLineItemId: (json['ParentLineItemID'] as num?)?.toInt() ?? 0, + itemName: (json['ItemName'] as String?) ?? '', + quantity: (json['Quantity'] as num?)?.toInt() ?? 1, + unitPrice: (json['UnitPrice'] as num?)?.toDouble() ?? 0.0, + remarks: (json['Remarks'] as String?) ?? '', + isDefault: json['IsDefault'] == true, + modifiers: modifiersJson + .map((e) => OrderLineItemDetail.fromJson(e as Map)) + .toList(), + ); + } + + /// Calculate total price for this item including modifiers + double get totalPrice { + double total = unitPrice * quantity; + for (final mod in modifiers) { + total += mod.unitPrice * mod.quantity; + } + return total; + } + + /// Check if this item has any non-default modifiers + bool get hasNonDefaultModifiers { + return modifiers.any((m) => !m.isDefault); + } + + /// Get only non-default modifiers + List get nonDefaultModifiers { + return modifiers.where((m) => !m.isDefault).toList(); + } +} diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart new file mode 100644 index 0000000..f75b3ed --- /dev/null +++ b/lib/screens/about_screen.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; + +class AboutScreen extends StatelessWidget { + const AboutScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('About Payfrit'), + ), + body: ListView( + padding: const EdgeInsets.all(24), + children: [ + // Logo/Icon + Center( + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + Icons.restaurant_menu, + size: 50, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + const SizedBox(height: 24), + + // App name + Center( + child: Text( + 'Payfrit', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 4), + + // Version + Center( + child: Text( + 'Version 0.1.0', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 32), + + // Description + Text( + 'Payfrit makes dining out easier. Order from your table, split the bill with friends, and pay without waiting.', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + + // Features section + _buildSectionHeader(context, 'Features'), + const SizedBox(height: 12), + _buildFeatureItem( + context, + Icons.qr_code_scanner, + 'Scan & Order', + 'Scan the table beacon to browse the menu and order directly from your phone', + ), + _buildFeatureItem( + context, + Icons.group, + 'Group Orders', + 'Invite friends to join your order and split the bill easily', + ), + _buildFeatureItem( + context, + Icons.delivery_dining, + 'Delivery & Takeaway', + 'Order for delivery or pick up when dining in isn\'t an option', + ), + _buildFeatureItem( + context, + Icons.payment, + 'Easy Payment', + 'Pay your share securely with just a few taps', + ), + + const SizedBox(height: 32), + + // Contact section + _buildSectionHeader(context, 'Contact'), + const SizedBox(height: 12), + ListTile( + leading: const Icon(Icons.email_outlined), + title: const Text('support@payfrit.com'), + contentPadding: EdgeInsets.zero, + ), + ListTile( + leading: const Icon(Icons.language), + title: const Text('www.payfrit.com'), + contentPadding: EdgeInsets.zero, + ), + + const SizedBox(height: 32), + + // Legal + Center( + child: Text( + '\u00a9 2025 Payfrit. All rights reserved.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ); + } + + Widget _buildSectionHeader(BuildContext context, String title) { + return Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ); + } + + Widget _buildFeatureItem( + BuildContext context, + IconData icon, + String title, + String description, + ) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + size: 24, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/account_screen.dart b/lib/screens/account_screen.dart index 8261c73..1b76d57 100644 --- a/lib/screens/account_screen.dart +++ b/lib/screens/account_screen.dart @@ -427,6 +427,17 @@ class _AccountScreenState extends State { const Divider(height: 1), + // About Payfrit + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('About Payfrit'), + subtitle: const Text('App info and features'), + trailing: const Icon(Icons.chevron_right), + onTap: () => Navigator.pushNamed(context, AppRoutes.about), + ), + + const Divider(height: 1), + const SizedBox(height: 24), // Sign Out button diff --git a/lib/screens/order_detail_screen.dart b/lib/screens/order_detail_screen.dart new file mode 100644 index 0000000..0070898 --- /dev/null +++ b/lib/screens/order_detail_screen.dart @@ -0,0 +1,514 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../models/order_detail.dart'; +import '../services/api.dart'; + +class OrderDetailScreen extends StatefulWidget { + final int orderId; + + const OrderDetailScreen({super.key, required this.orderId}); + + @override + State createState() => _OrderDetailScreenState(); +} + +class _OrderDetailScreenState extends State { + OrderDetail? _order; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadOrder(); + } + + Future _loadOrder() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final order = await Api.getOrderDetail(orderId: widget.orderId); + if (mounted) { + setState(() { + _order = order; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Error loading order detail: $e'); + if (mounted) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Order #${widget.orderId}'), + ), + body: _buildBody(), + ); + } + + Widget _buildBody() { + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (_error != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 16), + Text( + 'Failed to load order', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + _error!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 24), + FilledButton.icon( + onPressed: _loadOrder, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ), + ), + ); + } + + final order = _order; + if (order == null) { + return const Center(child: Text('Order not found')); + } + + return RefreshIndicator( + onRefresh: _loadOrder, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildOrderHeader(order), + const SizedBox(height: 16), + _buildStatusCard(order), + const SizedBox(height: 16), + _buildItemsCard(order), + const SizedBox(height: 16), + _buildTotalsCard(order), + if (order.notes.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildNotesCard(order), + ], + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildOrderHeader(OrderDetail order) { + final dateFormat = DateFormat('MMM d, yyyy'); + final timeFormat = DateFormat('h:mm a'); + final displayDate = order.submittedOn ?? order.createdOn; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + order.businessName.isNotEmpty ? order.businessName : 'Order', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + _buildOrderTypeChip(order.orderTypeId, order.orderTypeName), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + '${dateFormat.format(displayDate)} at ${timeFormat.format(displayDate)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + if (order.servicePoint.name.isNotEmpty) ...[ + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.table_restaurant, + size: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + order.servicePoint.name, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ], + ), + ), + ); + } + + Widget _buildOrderTypeChip(int typeId, String typeName) { + IconData icon; + Color color; + + switch (typeId) { + case 1: // Dine-in + icon = Icons.restaurant; + color = Colors.orange; + break; + case 2: // Takeaway + icon = Icons.shopping_bag; + color = Colors.blue; + break; + case 3: // Delivery + icon = Icons.delivery_dining; + color = Colors.green; + break; + default: + icon = Icons.receipt; + color = Colors.grey; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 4), + Text( + typeName, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildStatusCard(OrderDetail order) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + _buildStatusIcon(order.status), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Status', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Text( + order.statusText, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatusIcon(int status) { + IconData icon; + Color color; + + switch (status) { + case 1: // Submitted + icon = Icons.send; + color = Colors.blue; + break; + case 2: // In Progress + icon = Icons.pending; + color = Colors.orange; + break; + case 3: // Ready + icon = Icons.check_circle_outline; + color = Colors.green; + break; + case 4: // Completed + icon = Icons.check_circle; + color = Colors.green; + break; + case 5: // Cancelled + icon = Icons.cancel; + color = Colors.red; + break; + default: // Cart or Unknown + icon = Icons.shopping_cart; + color = Colors.grey; + } + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 24), + ); + } + + Widget _buildItemsCard(OrderDetail order) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Items', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + ...order.lineItems.map((item) => _buildLineItem(item)), + ], + ), + ), + ); + } + + Widget _buildLineItem(OrderLineItemDetail item) { + final nonDefaultMods = item.nonDefaultModifiers; + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quantity badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${item.quantity}x', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 8), + // Item name + Expanded( + child: Text( + item.itemName, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ), + // Price + Text( + '\$${item.totalPrice.toStringAsFixed(2)}', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ], + ), + // Non-default modifiers + if (nonDefaultMods.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.only(left: 36), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: nonDefaultMods.map((mod) { + final modPrice = mod.unitPrice > 0 + ? ' (+\$${mod.unitPrice.toStringAsFixed(2)})' + : ''; + return Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + '+ ${mod.itemName}$modPrice', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ); + }).toList(), + ), + ), + ], + // Item remarks/notes + if (item.remarks.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.only(left: 36), + child: Text( + '"${item.remarks}"', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.orange[700], + fontStyle: FontStyle.italic, + ), + ), + ), + ], + const Divider(height: 16), + ], + ), + ); + } + + Widget _buildTotalsCard(OrderDetail order) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildTotalRow('Subtotal', order.subtotal), + const SizedBox(height: 8), + _buildTotalRow('Tax', order.tax), + if (order.tip > 0) ...[ + const SizedBox(height: 8), + _buildTotalRow('Tip', order.tip), + ], + const Divider(height: 16), + _buildTotalRow('Total', order.total, isTotal: true), + ], + ), + ), + ); + } + + Widget _buildTotalRow(String label, double amount, {bool isTotal = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: isTotal + ? Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ) + : Theme.of(context).textTheme.bodyMedium, + ), + Text( + '\$${amount.toStringAsFixed(2)}', + style: isTotal + ? Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ) + : Theme.of(context).textTheme.bodyMedium, + ), + ], + ); + } + + Widget _buildNotesCard(OrderDetail order) { + return Card( + color: Colors.amber[50], + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.note, color: Colors.amber[700], size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Order Notes', + style: TextStyle( + fontSize: 12, + color: Colors.amber[800], + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + order.notes, + style: TextStyle( + fontSize: 14, + color: Colors.amber[900], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/order_history_screen.dart b/lib/screens/order_history_screen.dart index bea95a7..0ef80e8 100644 --- a/lib/screens/order_history_screen.dart +++ b/lib/screens/order_history_screen.dart @@ -3,6 +3,7 @@ import 'package:intl/intl.dart'; import '../models/order_history.dart'; import '../services/api.dart'; +import 'order_detail_screen.dart'; class OrderHistoryScreen extends StatefulWidget { const OrderHistoryScreen({super.key}); @@ -156,9 +157,11 @@ class _OrderCard extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: InkWell( onTap: () { - // TODO: Navigate to order detail - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Order details coming soon')), + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => OrderDetailScreen(orderId: order.orderId), + ), ); }, borderRadius: BorderRadius.circular(12), diff --git a/lib/services/api.dart b/lib/services/api.dart index c710103..bf4b350 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -3,6 +3,7 @@ import "package:http/http.dart" as http; import "../models/cart.dart"; import "../models/menu_item.dart"; +import "../models/order_detail.dart"; import "../models/order_history.dart"; import "../models/restaurant.dart"; import "../models/service_point.dart"; @@ -754,6 +755,19 @@ class Api { final userData = j["USER"] as Map? ?? {}; return UserProfile.fromJson(userData); } + + /// Get order detail by ID + static Future getOrderDetail({required int orderId}) async { + final raw = await _getRaw("/orders/getDetail.cfm?OrderID=$orderId"); + final j = _requireJson(raw, "GetOrderDetail"); + + if (!_ok(j)) { + throw StateError("GetOrderDetail failed: ${_err(j)}"); + } + + final orderData = j["ORDER"] as Map? ?? {}; + return OrderDetail.fromJson(orderData); + } } class OrderHistoryResponse {