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 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-09 12:41:44 -08:00
parent 7b08fa73de
commit 77e3145175
7 changed files with 955 additions and 3 deletions

View file

@ -1,6 +1,7 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "../screens/account_screen.dart"; import "../screens/account_screen.dart";
import "../screens/about_screen.dart";
import "../screens/address_edit_screen.dart"; import "../screens/address_edit_screen.dart";
import "../screens/address_list_screen.dart"; import "../screens/address_list_screen.dart";
import "../screens/beacon_scan_screen.dart"; import "../screens/beacon_scan_screen.dart";
@ -30,6 +31,7 @@ class AppRoutes {
static const String profileSettings = "/profile-settings"; static const String profileSettings = "/profile-settings";
static const String addressList = "/addresses"; static const String addressList = "/addresses";
static const String addressEdit = "/address-edit"; static const String addressEdit = "/address-edit";
static const String about = "/about";
static Map<String, WidgetBuilder> get routes => { static Map<String, WidgetBuilder> get routes => {
splash: (_) => const SplashScreen(), splash: (_) => const SplashScreen(),
@ -47,5 +49,6 @@ class AppRoutes {
profileSettings: (_) => const ProfileSettingsScreen(), profileSettings: (_) => const ProfileSettingsScreen(),
addressList: (_) => const AddressListScreen(), addressList: (_) => const AddressListScreen(),
addressEdit: (_) => const AddressEditScreen(), addressEdit: (_) => const AddressEditScreen(),
about: (_) => const AboutScreen(),
}; };
} }

View file

@ -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<OrderLineItemDetail> 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<String, dynamic> json) {
final lineItemsJson = json['LineItems'] as List<dynamic>? ?? [];
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<String, dynamic>? ?? {}),
servicePoint: OrderServicePoint.fromJson(json['ServicePoint'] as Map<String, dynamic>? ?? {}),
lineItems: lineItemsJson
.map((e) => OrderLineItemDetail.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
/// Get only non-default modifiers for display
List<OrderLineItemDetail> 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<String, dynamic> 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<String, dynamic> 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<OrderLineItemDetail> 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<String, dynamic> json) {
final modifiersJson = json['Modifiers'] as List<dynamic>? ?? [];
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<String, dynamic>))
.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<OrderLineItemDetail> get nonDefaultModifiers {
return modifiers.where((m) => !m.isDefault).toList();
}
}

View file

@ -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,
),
),
],
),
),
],
),
);
}
}

View file

@ -427,6 +427,17 @@ class _AccountScreenState extends State<AccountScreen> {
const Divider(height: 1), 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), const SizedBox(height: 24),
// Sign Out button // Sign Out button

View file

@ -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<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends State<OrderDetailScreen> {
OrderDetail? _order;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadOrder();
}
Future<void> _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],
),
),
],
),
),
],
),
),
);
}
}

View file

@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
import '../models/order_history.dart'; import '../models/order_history.dart';
import '../services/api.dart'; import '../services/api.dart';
import 'order_detail_screen.dart';
class OrderHistoryScreen extends StatefulWidget { class OrderHistoryScreen extends StatefulWidget {
const OrderHistoryScreen({super.key}); const OrderHistoryScreen({super.key});
@ -156,9 +157,11 @@ class _OrderCard extends StatelessWidget {
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
// TODO: Navigate to order detail Navigator.push(
ScaffoldMessenger.of(context).showSnackBar( context,
const SnackBar(content: Text('Order details coming soon')), MaterialPageRoute(
builder: (_) => OrderDetailScreen(orderId: order.orderId),
),
); );
}, },
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),

View file

@ -3,6 +3,7 @@ import "package:http/http.dart" as http;
import "../models/cart.dart"; import "../models/cart.dart";
import "../models/menu_item.dart"; import "../models/menu_item.dart";
import "../models/order_detail.dart";
import "../models/order_history.dart"; import "../models/order_history.dart";
import "../models/restaurant.dart"; import "../models/restaurant.dart";
import "../models/service_point.dart"; import "../models/service_point.dart";
@ -754,6 +755,19 @@ class Api {
final userData = j["USER"] as Map<String, dynamic>? ?? {}; final userData = j["USER"] as Map<String, dynamic>? ?? {};
return UserProfile.fromJson(userData); return UserProfile.fromJson(userData);
} }
/// Get order detail by ID
static Future<OrderDetail> 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<String, dynamic>? ?? {};
return OrderDetail.fromJson(orderData);
}
} }
class OrderHistoryResponse { class OrderHistoryResponse {