Update: 2026-08-02 17:52:28
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
// food_cart_page.dart — مراجعة السلة، عرض السعر الموقّع، وإنشاء الطلب
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../views/widgets/error_snakbar.dart';
|
||||
import '../../controller/food/food_controller.dart';
|
||||
import '../../controller/food/food_models.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import 'food_order_tracking_page.dart';
|
||||
|
||||
class FoodCartPage extends StatefulWidget {
|
||||
const FoodCartPage({super.key});
|
||||
|
||||
@override
|
||||
State<FoodCartPage> createState() => _FoodCartPageState();
|
||||
}
|
||||
|
||||
class _FoodCartPageState extends State<FoodCartPage> {
|
||||
final TextEditingController _addressController = TextEditingController();
|
||||
final TextEditingController _noteController = TextEditingController();
|
||||
String _paymentMethod = 'wallet';
|
||||
Position? _position;
|
||||
bool _isFetchingLocation = false;
|
||||
|
||||
bool get _isAr => box.read(BoxName.lang) == 'ar';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchLocation();
|
||||
}
|
||||
|
||||
Future<void> _fetchLocation() async {
|
||||
setState(() => _isFetchingLocation = true);
|
||||
try {
|
||||
final permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
await Geolocator.requestPermission();
|
||||
}
|
||||
_position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||
} catch (_) {
|
||||
_position = null;
|
||||
}
|
||||
if (mounted) setState(() => _isFetchingLocation = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<FoodController>(
|
||||
builder: (c) => MyScafolld(
|
||||
title: _isAr ? 'السلة' : 'Cart',
|
||||
isleading: true,
|
||||
body: [
|
||||
c.cartLines.isEmpty ? _emptyCart() : _cartContent(c),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _emptyCart() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.shopping_cart_outlined, size: 72, color: AppColor.grayColor),
|
||||
const SizedBox(height: 16),
|
||||
Text(_isAr ? 'سلتك فارغة' : 'Your cart is empty', style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cartContent(FoodController c) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
...c.cartLines.map((line) => _cartLineTile(c, line)),
|
||||
const SizedBox(height: 16),
|
||||
_summaryRow(_isAr ? 'المجموع الفرعي' : 'Subtotal', foodFormatPrice(c.cartTotal)),
|
||||
if (c.currentQuote != null) ...[
|
||||
_summaryRow(_isAr ? 'رسوم التوصيل' : 'Delivery fee', foodFormatPrice(c.currentQuote!.deliveryFee)),
|
||||
const Divider(),
|
||||
_summaryRow(_isAr ? 'الإجمالي' : 'Total', foodFormatPrice(c.currentQuote!.grandTotal), bold: true),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
controller: _addressController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _isAr ? 'عنوان التوصيل' : 'Delivery address',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
suffixIcon: _isFetchingLocation
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
)
|
||||
: Icon(_position != null ? Icons.my_location_rounded : Icons.location_off_rounded,
|
||||
color: _position != null ? AppColor.greenColor : AppColor.redColor),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _noteController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _isAr ? 'ملاحظات (اختياري)' : 'Notes (optional)',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_paymentMethodPicker(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, MediaQuery.of(context).padding.bottom + 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
onPressed: c.isQuoting || c.isPlacingOrder ? null : () => _onCheckoutPressed(c),
|
||||
child: c.isPlacingOrder
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: Text(
|
||||
c.currentQuote == null
|
||||
? (_isAr ? 'احسب السعر' : 'Get Quote')
|
||||
: '${_isAr ? "اطلب الآن" : "Place Order"} · ${foodFormatPrice(c.currentQuote!.grandTotal)}',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onCheckoutPressed(FoodController c) async {
|
||||
if (c.currentQuote == null) {
|
||||
await c.refreshQuote();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_addressController.text.trim().isEmpty) {
|
||||
mySnackbarWarning(_isAr ? 'أدخل عنوان التوصيل' : 'Enter a delivery address');
|
||||
return;
|
||||
}
|
||||
if (_position == null) {
|
||||
mySnackbarWarning(_isAr ? 'تعذّر تحديد موقعك — فعّل خدمة الموقع' : 'Could not get your location — enable location services');
|
||||
return;
|
||||
}
|
||||
|
||||
final orderId = await c.placeOrder(
|
||||
deliveryAddress: _addressController.text.trim(),
|
||||
deliveryLat: _position!.latitude,
|
||||
deliveryLng: _position!.longitude,
|
||||
paymentMethod: _paymentMethod,
|
||||
customerNote: _noteController.text.trim(),
|
||||
);
|
||||
|
||||
if (orderId != null) {
|
||||
Get.offAll(() => FoodOrderTrackingPage(orderId: orderId));
|
||||
}
|
||||
}
|
||||
|
||||
Widget _cartLineTile(FoodController c, FoodCartLine line) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
color: AppColor.cardColor,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: AppColor.borderColor),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(line.item.nameAr, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(foodFormatPrice(line.lineTotal), style: AppStyle.subtitle.copyWith(color: AppColor.accentColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline_rounded),
|
||||
onPressed: () => c.updateQuantity(line.lineKey, line.quantity - 1),
|
||||
),
|
||||
Text(line.quantity.toString(), style: AppStyle.title),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline_rounded),
|
||||
onPressed: () => c.updateQuantity(line.lineKey, line.quantity + 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _summaryRow(String label, String value, {bool bold = false}) {
|
||||
final style = bold
|
||||
? AppStyle.title.copyWith(fontWeight: FontWeight.bold, fontSize: 18)
|
||||
: AppStyle.subtitle;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [Text(label, style: style), Text(value, style: style)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _paymentMethodPicker() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(_isAr ? 'طريقة الدفع' : 'Payment method', style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
RadioListTile<String>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: 'wallet',
|
||||
groupValue: _paymentMethod,
|
||||
title: Text(_isAr ? 'المحفظة' : 'Wallet'),
|
||||
onChanged: (v) => setState(() => _paymentMethod = v!),
|
||||
),
|
||||
RadioListTile<String>(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: 'cash',
|
||||
groupValue: _paymentMethod,
|
||||
title: Text(_isAr ? 'نقداً عند الاستلام' : 'Cash on delivery'),
|
||||
onChanged: (v) => setState(() => _paymentMethod = v!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
// food_home_page.dart — الصفحة الرئيسية لتبويب "طعام": تصفح المطاعم
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../controller/food/food_controller.dart';
|
||||
import '../../controller/food/food_models.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import 'food_merchant_page.dart';
|
||||
import 'food_cart_page.dart';
|
||||
import 'food_order_history_page.dart';
|
||||
|
||||
class FoodHomePage extends StatelessWidget {
|
||||
const FoodHomePage({super.key});
|
||||
|
||||
bool get _isAr => box.read(BoxName.lang) == 'ar';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = Get.put(FoodController());
|
||||
c.fetchMerchants();
|
||||
|
||||
return GetBuilder<FoodController>(
|
||||
builder: (c) => MyScafolld(
|
||||
title: _isAr ? 'طعام' : 'Food',
|
||||
isleading: true,
|
||||
action: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.history_rounded, color: AppColor.primaryColor),
|
||||
onPressed: () => Get.to(() => const FoodOrderHistoryPage()),
|
||||
),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.shopping_cart_rounded, color: AppColor.primaryColor),
|
||||
onPressed: () => Get.to(() => const FoodCartPage()),
|
||||
),
|
||||
if (c.cartItemsCount > 0)
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(color: AppColor.redColor, shape: BoxShape.circle),
|
||||
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
|
||||
child: Text(
|
||||
c.cartItemsCount.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: [
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: _citySearchBar(c),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => c.fetchMerchants(),
|
||||
child: c.isLoadingMerchants
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: c.merchants.isEmpty
|
||||
? _emptyState()
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 24),
|
||||
itemCount: c.merchants.length,
|
||||
itemBuilder: (_, i) => _merchantCard(c.merchants[i]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _citySearchBar(FoodController c) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => _showCityPicker(c),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.borderColor),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.location_on_rounded, color: AppColor.accentColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(c.city, style: AppStyle.title, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
Icon(Icons.keyboard_arrow_down_rounded, color: AppColor.grayColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () => _showSearchDialog(c),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.borderColor),
|
||||
),
|
||||
child: Icon(Icons.search_rounded, color: AppColor.primaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showCityPicker(FoodController c) {
|
||||
final controller = TextEditingController(text: c.city);
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(_isAr ? 'اختر المدينة' : 'Select City'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(hintText: _isAr ? 'اسم المدينة' : 'City name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Get.back(), child: Text(_isAr ? 'إلغاء' : 'Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
c.setCity(controller.text);
|
||||
},
|
||||
child: Text(_isAr ? 'تم' : 'Done'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSearchDialog(FoodController c) {
|
||||
final controller = TextEditingController();
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(_isAr ? 'بحث عن مطعم أو صنف' : 'Search restaurant or dish'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(hintText: _isAr ? 'اكتب هنا...' : 'Type here...'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Get.back(), child: Text(_isAr ? 'إلغاء' : 'Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
c.searchMerchants(controller.text);
|
||||
},
|
||||
child: Text(_isAr ? 'بحث' : 'Search'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _emptyState() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
Icon(Icons.restaurant_menu_rounded, size: 72, color: AppColor.grayColor),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_isAr ? 'لا توجد مطاعم متاحة في هذه المدينة حالياً' : 'No restaurants available in this city yet',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _merchantCard(FoodMerchant m) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: AppColor.cardColor,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: AppColor.borderColor),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => Get.to(() => FoodMerchantPage(merchantId: m.id)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (m.coverUrl != null && m.coverUrl!.isNotEmpty)
|
||||
Image.network(m.coverUrl!, height: 120, width: double.infinity, fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(height: 120, color: AppColor.borderColor)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppColor.accentColor.withOpacity(0.15),
|
||||
backgroundImage: (m.logoUrl != null && m.logoUrl!.isNotEmpty)
|
||||
? NetworkImage(m.logoUrl!)
|
||||
: null,
|
||||
child: (m.logoUrl == null || m.logoUrl!.isEmpty)
|
||||
? Icon(Icons.restaurant_rounded, color: AppColor.accentColor)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(m.nameAr, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star_rounded, color: Color(0xFFF59E0B), size: 16),
|
||||
const SizedBox(width: 2),
|
||||
Text(m.ratingAvg.toStringAsFixed(1), style: AppStyle.subtitle),
|
||||
const SizedBox(width: 10),
|
||||
Icon(Icons.access_time_rounded, size: 14, color: AppColor.grayColor),
|
||||
const SizedBox(width: 2),
|
||||
Text('${m.avgPrepMinutes} ${_isAr ? "د" : "min"}', style: AppStyle.subtitle),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!m.isOpen)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(_isAr ? 'مغلق' : 'Closed',
|
||||
style: TextStyle(color: AppColor.redColor, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// food_merchant_page.dart — قائمة مطعم واحد + إضافة أصناف للسلة
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../views/widgets/error_snakbar.dart';
|
||||
import '../../controller/food/food_controller.dart';
|
||||
import '../../controller/food/food_models.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import 'food_cart_page.dart';
|
||||
|
||||
class FoodMerchantPage extends StatelessWidget {
|
||||
final int merchantId;
|
||||
const FoodMerchantPage({super.key, required this.merchantId});
|
||||
|
||||
bool get _isAr => box.read(BoxName.lang) == 'ar';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = Get.find<FoodController>();
|
||||
c.openMerchant(merchantId);
|
||||
|
||||
return GetBuilder<FoodController>(
|
||||
builder: (c) {
|
||||
final merchant = c.selectedMerchant;
|
||||
return MyScafolld(
|
||||
title: merchant?.nameAr ?? (_isAr ? 'المطعم' : 'Restaurant'),
|
||||
isleading: true,
|
||||
action: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.shopping_cart_rounded, color: AppColor.primaryColor),
|
||||
onPressed: () => Get.to(() => const FoodCartPage()),
|
||||
),
|
||||
if (c.cartItemsCount > 0)
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(color: AppColor.redColor, shape: BoxShape.circle),
|
||||
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
|
||||
child: Text(c.cartItemsCount.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: [
|
||||
c.isLoadingMenu
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: merchant == null
|
||||
? Center(child: Text(_isAr ? 'تعذر تحميل المطعم' : 'Failed to load restaurant'))
|
||||
: _content(c, merchant),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _content(FoodController c, FoodMerchant merchant) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
children: [
|
||||
if (!merchant.isOpen)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_isAr ? 'هذا المطعم مغلق حالياً' : 'This restaurant is currently closed',
|
||||
style: TextStyle(color: AppColor.redColor, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (merchant.descriptionAr != null && merchant.descriptionAr!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(merchant.descriptionAr!, style: AppStyle.subtitle),
|
||||
),
|
||||
Text(
|
||||
_isAr
|
||||
? 'الحد الأدنى للطلب: ${(merchant.minOrderAmount / 1000).toStringAsFixed(3)} د.أ'
|
||||
: 'Minimum order: ${(merchant.minOrderAmount / 1000).toStringAsFixed(3)} JOD',
|
||||
style: AppStyle.subtitle.copyWith(color: AppColor.grayColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...c.categories.map((cat) => _categorySection(c, merchant, cat)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _categorySection(FoodController c, FoodMerchant merchant, FoodMenuCategory cat) {
|
||||
if (cat.items.isEmpty) return const SizedBox.shrink();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(cat.nameAr, style: AppStyle.headTitle2.copyWith(fontSize: 18)),
|
||||
const SizedBox(height: 8),
|
||||
...cat.items.map((item) => _itemTile(c, merchant, item)),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _itemTile(FoodController c, FoodMerchant merchant, FoodMenuItem item) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
color: AppColor.cardColor,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: AppColor.borderColor),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: !item.isAvailable || !merchant.isOpen
|
||||
? null
|
||||
: () => _openItemSheet(c, merchant, item),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.nameAr, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
if (item.descriptionAr != null && item.descriptionAr!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(item.descriptionAr!,
|
||||
style: AppStyle.subtitle, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
const SizedBox(height: 6),
|
||||
Text(foodFormatPrice(item.price),
|
||||
style: AppStyle.title.copyWith(color: AppColor.accentColor, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (item.imageUrl != null && item.imageUrl!.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(item.imageUrl!, width: 72, height: 72, fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(width: 72, height: 72, color: AppColor.borderColor)),
|
||||
),
|
||||
if (!item.isAvailable)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(_isAr ? 'غير متاح' : 'Unavailable',
|
||||
style: TextStyle(color: AppColor.redColor, fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openItemSheet(FoodController c, FoodMerchant merchant, FoodMenuItem item) {
|
||||
final Map<int, List<String>> selected = {};
|
||||
int quantity = 1;
|
||||
|
||||
Get.bottomSheet(
|
||||
isScrollControlled: true,
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
int unitPrice = item.price;
|
||||
for (final group in item.options) {
|
||||
for (final choiceId in (selected[group.id] ?? [])) {
|
||||
final choice = group.choices.firstWhere((ch) => ch.id == choiceId,
|
||||
orElse: () => FoodOptionChoice(id: '', labelAr: '', price: 0));
|
||||
unitPrice += choice.price;
|
||||
}
|
||||
}
|
||||
|
||||
bool canSubmit = true;
|
||||
for (final group in item.options) {
|
||||
if (group.isRequired && (selected[group.id] ?? []).isEmpty) canSubmit = false;
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: Get.height * 0.85),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(width: 40, height: 4,
|
||||
decoration: BoxDecoration(color: Colors.grey.withOpacity(0.3), borderRadius: BorderRadius.circular(2))),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Text(item.nameAr, style: AppStyle.headTitle2.copyWith(fontSize: 20)),
|
||||
if (item.descriptionAr != null && item.descriptionAr!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(item.descriptionAr!, style: AppStyle.subtitle),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
...item.options.map((group) => _optionGroupWidget(group, selected, setState)),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline_rounded),
|
||||
onPressed: quantity > 1 ? () => setState(() => quantity--) : null,
|
||||
),
|
||||
Text(quantity.toString(), style: AppStyle.headTitle2.copyWith(fontSize: 20)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline_rounded),
|
||||
onPressed: () => setState(() => quantity++),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 8, 20, MediaQuery.of(context).padding.bottom + 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: canSubmit ? AppColor.primaryColor : AppColor.grayColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
onPressed: !canSubmit
|
||||
? null
|
||||
: () {
|
||||
final ok = c.addToCartForMerchant(merchant.id, item,
|
||||
quantity: quantity, options: selected);
|
||||
Get.back();
|
||||
if (ok) {
|
||||
mySnackbarSuccess(_isAr ? 'أُضيف إلى السلة' : 'Added to cart');
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'${_isAr ? "إضافة" : "Add"} · ${foodFormatPrice(unitPrice * quantity)}',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _optionGroupWidget(
|
||||
FoodItemOptionGroup group, Map<int, List<String>> selected, StateSetter setState) {
|
||||
final isSingle = group.maxSelect <= 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(group.groupNameAr, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
if (group.isRequired) ...[
|
||||
const SizedBox(width: 6),
|
||||
Text(_isAr ? '(مطلوب)' : '(required)',
|
||||
style: TextStyle(color: AppColor.redColor, fontSize: 12)),
|
||||
],
|
||||
],
|
||||
),
|
||||
...group.choices.map((choice) {
|
||||
final isChecked = (selected[group.id] ?? []).contains(choice.id);
|
||||
return CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
value: isChecked,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(choice.labelAr),
|
||||
secondary: choice.price > 0 ? Text('+${foodFormatPrice(choice.price)}') : null,
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
final list = List<String>.from(selected[group.id] ?? []);
|
||||
if (checked == true) {
|
||||
if (isSingle) list.clear();
|
||||
if (!isSingle && list.length >= group.maxSelect) return;
|
||||
list.add(choice.id);
|
||||
} else {
|
||||
list.remove(choice.id);
|
||||
}
|
||||
selected[group.id] = list;
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// food_order_history_page.dart — سجل طلبات الطعام
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../controller/food/food_models.dart';
|
||||
import '../../controller/food/food_service.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import 'food_order_tracking_page.dart';
|
||||
|
||||
class FoodOrderHistoryPage extends StatefulWidget {
|
||||
const FoodOrderHistoryPage({super.key});
|
||||
|
||||
@override
|
||||
State<FoodOrderHistoryPage> createState() => _FoodOrderHistoryPageState();
|
||||
}
|
||||
|
||||
class _FoodOrderHistoryPageState extends State<FoodOrderHistoryPage> {
|
||||
bool get _isAr => box.read(BoxName.lang) == 'ar';
|
||||
bool _isLoading = true;
|
||||
List<FoodOrder> _orders = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() => _isLoading = true);
|
||||
final res = await FoodService.getHistory();
|
||||
setState(() {
|
||||
_orders = res.data ?? [];
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: _isAr ? 'سجل الطلبات' : 'Order History',
|
||||
isleading: true,
|
||||
body: [
|
||||
_isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _orders.isEmpty
|
||||
? Center(
|
||||
child: Text(_isAr ? 'لا يوجد طلبات سابقة' : 'No past orders', style: AppStyle.title),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _orders.length,
|
||||
itemBuilder: (_, i) => _orderTile(_orders[i]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _orderTile(FoodOrder order) {
|
||||
final isCancelled = foodOrderIsCancelled(order.status);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
color: AppColor.cardColor,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: AppColor.borderColor),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.accentColor.withOpacity(0.15),
|
||||
backgroundImage: (order.merchantLogoUrl != null && order.merchantLogoUrl!.isNotEmpty)
|
||||
? NetworkImage(order.merchantLogoUrl!)
|
||||
: null,
|
||||
child: (order.merchantLogoUrl == null || order.merchantLogoUrl!.isEmpty)
|
||||
? Icon(Icons.restaurant_rounded, color: AppColor.accentColor)
|
||||
: null,
|
||||
),
|
||||
title: Text(order.merchantNameAr ?? '#${order.id}', style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(
|
||||
isCancelled
|
||||
? (_isAr ? 'مُلغى' : 'Cancelled')
|
||||
: order.status == 'delivered'
|
||||
? (_isAr ? 'تم التسليم' : 'Delivered')
|
||||
: (_isAr ? 'قيد التنفيذ' : 'In progress'),
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: isCancelled ? AppColor.redColor : (order.status == 'delivered' ? AppColor.greenColor : AppColor.accentColor),
|
||||
),
|
||||
),
|
||||
trailing: Text(foodFormatPrice(order.grandTotal), style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
onTap: () => Get.to(() => FoodOrderTrackingPage(orderId: order.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// food_order_tracking_page.dart — تتبّع حالة الطلب (polling — نفس مصدر الحقيقة في backend)
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../controller/food/food_controller.dart';
|
||||
import '../../controller/food/food_models.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import 'food_home_page.dart';
|
||||
|
||||
class FoodOrderTrackingPage extends StatefulWidget {
|
||||
final int orderId;
|
||||
const FoodOrderTrackingPage({super.key, required this.orderId});
|
||||
|
||||
@override
|
||||
State<FoodOrderTrackingPage> createState() => _FoodOrderTrackingPageState();
|
||||
}
|
||||
|
||||
class _FoodOrderTrackingPageState extends State<FoodOrderTrackingPage> {
|
||||
bool get _isAr => box.read(BoxName.lang) == 'ar';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Get.find<FoodController>().startTrackingOrder(widget.orderId);
|
||||
}
|
||||
|
||||
static const Map<String, String> _statusLabelsAr = {
|
||||
'pending': 'بانتظار قبول المطعم',
|
||||
'merchant_accepted': 'المطعم قبل طلبك',
|
||||
'preparing': 'جاري التحضير',
|
||||
'ready': 'جاهز، بانتظار سائق',
|
||||
'courier_assigned': 'تم تعيين سائق',
|
||||
'picked_up': 'السائق في الطريق إليك',
|
||||
'delivered': 'تم التسليم',
|
||||
'rejected': 'رُفض الطلب',
|
||||
'cancelled_by_customer': 'أُلغي الطلب',
|
||||
'cancelled_by_merchant': 'أُلغي من المطعم',
|
||||
'cancelled_system': 'أُلغي تلقائياً',
|
||||
};
|
||||
|
||||
static const Map<String, String> _statusLabelsEn = {
|
||||
'pending': 'Awaiting merchant',
|
||||
'merchant_accepted': 'Accepted by restaurant',
|
||||
'preparing': 'Preparing',
|
||||
'ready': 'Ready, waiting for courier',
|
||||
'courier_assigned': 'Courier assigned',
|
||||
'picked_up': 'Courier on the way',
|
||||
'delivered': 'Delivered',
|
||||
'rejected': 'Order rejected',
|
||||
'cancelled_by_customer': 'Cancelled',
|
||||
'cancelled_by_merchant': 'Cancelled by restaurant',
|
||||
'cancelled_system': 'Auto-cancelled',
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<FoodController>(
|
||||
builder: (c) {
|
||||
final order = c.activeOrder;
|
||||
return MyScafolld(
|
||||
title: _isAr ? 'طلبك #${widget.orderId}' : 'Order #${widget.orderId}',
|
||||
isleading: true,
|
||||
body: [
|
||||
order == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _content(c, order),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _content(FoodController c, FoodOrder order) {
|
||||
final isCancelled = foodOrderIsCancelled(order.status);
|
||||
final label = _isAr ? _statusLabelsAr[order.status] : _statusLabelsEn[order.status];
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isCancelled ? AppColor.redColor.withOpacity(0.08) : AppColor.accentColor.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
isCancelled
|
||||
? Icons.cancel_rounded
|
||||
: order.status == 'delivered'
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.delivery_dining_rounded,
|
||||
size: 48,
|
||||
color: isCancelled ? AppColor.redColor : AppColor.accentColor,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(label ?? order.status,
|
||||
style: AppStyle.headTitle2.copyWith(fontSize: 18), textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (!isCancelled) _statusTimeline(order.status),
|
||||
const SizedBox(height: 20),
|
||||
Text(_isAr ? 'ملخص الطلب' : 'Order Summary', style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
...order.items.map((i) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(child: Text('${i.quantity}× ${i.nameArSnapshot}', style: AppStyle.subtitle)),
|
||||
Text(foodFormatPrice(i.lineTotal), style: AppStyle.subtitle),
|
||||
],
|
||||
),
|
||||
)),
|
||||
const Divider(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(_isAr ? 'الإجمالي' : 'Total', style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(foodFormatPrice(order.grandTotal), style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (order.status == 'pending')
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: AppColor.redColor),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
onPressed: () => _confirmCancel(c),
|
||||
child: Text(_isAr ? 'إلغاء الطلب' : 'Cancel Order', style: TextStyle(color: AppColor.redColor)),
|
||||
),
|
||||
),
|
||||
if (order.status == 'delivered' && order.rating == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
),
|
||||
onPressed: () => _showRatingDialog(c, order.id),
|
||||
child: Text(_isAr ? 'قيّم الطلب' : 'Rate Order', style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (order.status == 'delivered' || isCancelled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () => Get.offAll(() => const FoodHomePage()),
|
||||
child: Text(_isAr ? 'العودة للمطاعم' : 'Back to restaurants'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusTimeline(String currentStatus) {
|
||||
final currentIndex = foodOrderStatusFlow.indexOf(currentStatus);
|
||||
return Column(
|
||||
children: List.generate(foodOrderStatusFlow.length, (i) {
|
||||
final status = foodOrderStatusFlow[i];
|
||||
final isDone = currentIndex >= i;
|
||||
final label = _isAr ? _statusLabelsAr[status] : _statusLabelsEn[status];
|
||||
return Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isDone ? AppColor.accentColor : AppColor.borderColor,
|
||||
),
|
||||
),
|
||||
if (i < foodOrderStatusFlow.length - 1)
|
||||
Container(width: 2, height: 28, color: isDone ? AppColor.accentColor : AppColor.borderColor),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(label ?? status,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: isDone ? AppColor.writeColor : AppColor.grayColor,
|
||||
fontWeight: isDone ? FontWeight.bold : FontWeight.normal)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmCancel(FoodController c) {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(_isAr ? 'إلغاء الطلب؟' : 'Cancel order?'),
|
||||
content: Text(_isAr
|
||||
? 'سيتم استرجاع المبلغ إلى محفظتك فوراً إن دفعت من المحفظة.'
|
||||
: 'Your wallet will be refunded immediately if paid by wallet.'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Get.back(), child: Text(_isAr ? 'تراجع' : 'Back')),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColor.redColor),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
c.cancelActiveOrder();
|
||||
},
|
||||
child: Text(_isAr ? 'إلغاء الطلب' : 'Cancel Order', style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRatingDialog(FoodController c, int orderId) {
|
||||
int rating = 5;
|
||||
Get.dialog(
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(_isAr ? 'قيّم تجربتك' : 'Rate your experience'),
|
||||
content: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(5, (i) {
|
||||
final star = i + 1;
|
||||
return IconButton(
|
||||
icon: Icon(star <= rating ? Icons.star_rounded : Icons.star_border_rounded,
|
||||
color: const Color(0xFFF59E0B), size: 32),
|
||||
onPressed: () => setState(() => rating = star),
|
||||
);
|
||||
}),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Get.back(), child: Text(_isAr ? 'لاحقاً' : 'Later')),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
await c.rateActiveOrder(rating);
|
||||
},
|
||||
child: Text(_isAr ? 'إرسال' : 'Submit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import '../HomePage/share_app_page.dart';
|
||||
import '../setting_page.dart';
|
||||
import '../profile/passenger_profile_page.dart';
|
||||
import '../../transit/transit_home_page.dart';
|
||||
import '../../food/food_home_page.dart';
|
||||
|
||||
// ─── ألوان النظام (Integrated with AppColor) ──────────────────────────────────
|
||||
Color get _kCyan => AppColor.cyanBlue;
|
||||
@@ -157,6 +158,11 @@ class MapMenuWidget extends StatelessWidget {
|
||||
onTap: () =>
|
||||
Get.to(() => const TransitHomePage()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'Food'.tr,
|
||||
icon: Icons.restaurant_rounded,
|
||||
onTap: () => Get.to(() => const FoodHomePage()),
|
||||
),
|
||||
MenuListItem(
|
||||
title: 'My Balance'.tr,
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user