Files
Siro/siro_driver/lib/views/food_delivery/food_offer_page.dart
T

440 lines
14 KiB
Dart

// food_offer_page.dart — شاشة استقبال عرض التوصيل (مستقلة عن شاشة عرض الرحلة)
//
// شاشة كاملة مقصودة لا بطاقة صغيرة: مهلة العرض 20 ثانية، والسائق يحتاج يقرأ
// المطعم والأجرة وهل التحصيل نقدي قبل أن يقرر. تفتح تلقائياً عند وصول العرض
// من سوكيت الطعام، وتُغلق نفسها إذا انتهت مهلة كل العروض.
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../controller/food_delivery/food_delivery_controller.dart';
import '../../controller/food_delivery/food_delivery_models.dart';
import '../../controller/functions/location_controller.dart';
class FoodOfferPage extends StatelessWidget {
const FoodOfferPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<FoodDeliveryController>(
builder: (c) {
if (c.pendingOffers.isEmpty) return const _NoOfferView();
final offer = c.pendingOffers.first;
final isResponding = c.isRespondingToOffer(offer.orderId);
return Scaffold(
backgroundColor: const Color(0xFF12171F),
body: SafeArea(
child: Column(
children: [
_Header(offer: offer, queued: c.pendingOffers.length),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const SizedBox(height: 8),
_FeeBadge(offer: offer),
const SizedBox(height: 20),
_RouteCard(offer: offer),
const SizedBox(height: 14),
_MetaRow(offer: offer),
if (offer.isCash) ...[
const SizedBox(height: 14),
_CashWarning(amount: offer.cashToCollect),
],
const SizedBox(height: 24),
],
),
),
),
_Actions(
isResponding: isResponding,
onAccept: () => c.respondToOffer(offer.orderId, true),
onReject: () => c.respondToOffer(offer.orderId, false),
),
],
),
),
);
},
);
}
}
class _NoOfferView extends StatelessWidget {
const _NoOfferView();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF12171F),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.timer_off_rounded, size: 64, color: Colors.white38),
const SizedBox(height: 12),
const Text('انتهت مهلة العرض',
style: TextStyle(color: Colors.white70, fontSize: 16)),
const SizedBox(height: 20),
TextButton(onPressed: Get.back, child: const Text('رجوع')),
],
),
),
);
}
}
class _Header extends StatelessWidget {
final FoodDeliveryOffer offer;
final int queued;
const _Header({required this.offer, required this.queued});
@override
Widget build(BuildContext context) {
final remaining = offer.secondsRemaining;
final progress = offer.ttlSeconds == 0 ? 0.0 : remaining / offer.ttlSeconds;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
child: Row(
children: [
SizedBox(
width: 62,
height: 62,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 62,
height: 62,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 5,
backgroundColor: Colors.white12,
valueColor: AlwaysStoppedAnimation(
remaining <= 5 ? const Color(0xFFE53935) : const Color(0xFFFF6B35),
),
),
),
Text('$remaining',
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20)),
],
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('🍔 عرض توصيل طلب طعام',
style: TextStyle(
color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
Text(
queued > 1 ? 'لديك $queued عروض بالانتظار' : 'وافق قبل انتهاء المهلة',
style: const TextStyle(color: Colors.white54, fontSize: 13),
),
],
),
),
],
),
);
}
}
class _FeeBadge extends StatelessWidget {
final FoodDeliveryOffer offer;
const _FeeBadge({required this.offer});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 18),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFF6B35), Color(0xFFF7931E)],
),
borderRadius: BorderRadius.circular(18),
),
child: Column(
children: [
const Text('أجرة التوصيل',
style: TextStyle(color: Colors.white70, fontSize: 13)),
const SizedBox(height: 4),
Text(foodFormatPrice(offer.deliveryFee),
style: const TextStyle(
color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold)),
],
),
);
}
}
class _RouteCard extends StatelessWidget {
final FoodDeliveryOffer offer;
const _RouteCard({required this.offer});
// مسافات تقريبية بخط مستقيم — كافية لقرار القبول خلال ثوانٍ، ولا نستدعي
// خدمة مسارات هنا حتى لا نضيف زمناً على مهلة عرض قصيرة أصلاً.
double? _distanceKm(double? aLat, double? aLng, double bLat, double bLng) {
if (aLat == null || aLng == null || bLat == 0 || bLng == 0) return null;
return Geolocator.distanceBetween(aLat, aLng, bLat, bLng) / 1000;
}
@override
Widget build(BuildContext context) {
double? driverLat;
double? driverLng;
if (Get.isRegistered<LocationController>()) {
final loc = Get.find<LocationController>().myLocation;
if (loc.latitude != 0) {
driverLat = loc.latitude;
driverLng = loc.longitude;
}
}
final toMerchant =
_distanceKm(driverLat, driverLng, offer.merchantLat ?? 0, offer.merchantLng ?? 0);
final merchantToCustomer =
_distanceKm(offer.merchantLat, offer.merchantLng, offer.deliveryLat, offer.deliveryLng);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.06),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white12),
),
child: Column(
children: [
_RouteRow(
icon: Icons.storefront_rounded,
iconColor: const Color(0xFFFF6B35),
title: offer.merchantNameAr,
subtitle: offer.merchantAddress ?? 'الاستلام من المطعم',
trailing: toMerchant == null ? null : '${toMerchant.toStringAsFixed(1)} كم',
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
const SizedBox(width: 18),
Container(width: 2, height: 22, color: Colors.white24),
],
),
),
_RouteRow(
icon: Icons.location_on_rounded,
iconColor: const Color(0xFF4CAF50),
title: 'التسليم للزبون',
// العنوان النصّي لا يصل في العرض — يظهر بعد القبول فقط (حماية بيانات الزبون)
subtitle: 'يظهر العنوان الكامل بعد قبول الطلب',
trailing: merchantToCustomer == null
? null
: '${merchantToCustomer.toStringAsFixed(1)} كم',
),
],
),
);
}
}
class _RouteRow extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String title;
final String subtitle;
final String? trailing;
const _RouteRow({
required this.icon,
required this.iconColor,
required this.title,
required this.subtitle,
this.trailing,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: iconColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis),
Text(subtitle,
style: const TextStyle(color: Colors.white54, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis),
],
),
),
if (trailing != null)
Text(trailing!,
style: const TextStyle(
color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)),
],
);
}
}
class _MetaRow extends StatelessWidget {
final FoodDeliveryOffer offer;
const _MetaRow({required this.offer});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: _MetaTile(
icon: Icons.shopping_bag_rounded,
label: 'عدد الأصناف',
value: offer.itemsCount > 0 ? '${offer.itemsCount}' : '—',
),
),
const SizedBox(width: 12),
Expanded(
child: _MetaTile(
icon: offer.isCash ? Icons.payments_rounded : Icons.account_balance_wallet_rounded,
label: 'طريقة الدفع',
value: offer.isCash ? 'نقداً' : 'محفظة',
),
),
],
);
}
}
class _MetaTile extends StatelessWidget {
final IconData icon;
final String label;
final String value;
const _MetaTile({required this.icon, required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.06),
borderRadius: BorderRadius.circular(14),
),
child: Column(
children: [
Icon(icon, color: Colors.white70, size: 20),
const SizedBox(height: 6),
Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11)),
Text(value,
style: const TextStyle(
color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold)),
],
),
);
}
}
class _CashWarning extends StatelessWidget {
final int amount;
const _CashWarning({required this.amount});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFF29900).withOpacity(0.15),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFF29900).withOpacity(0.5)),
),
child: Row(
children: [
const Icon(Icons.info_rounded, color: Color(0xFFF29900)),
const SizedBox(width: 10),
Expanded(
child: Text(
'طلب نقدي — ستحصّل ${foodFormatPrice(amount)} من الزبون عند التسليم',
style: const TextStyle(color: Color(0xFFFFD08A), fontSize: 13),
),
),
],
),
);
}
}
class _Actions extends StatelessWidget {
final bool isResponding;
final VoidCallback onAccept;
final VoidCallback onReject;
const _Actions({
required this.isResponding,
required this.onAccept,
required this.onReject,
});
@override
Widget build(BuildContext context) {
if (isResponding) {
return const Padding(
padding: EdgeInsets.all(24),
child: Center(child: CircularProgressIndicator(color: Color(0xFFFF6B35))),
);
}
return Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 56,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.white24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: onReject,
child: const Text('رفض',
style: TextStyle(color: Colors.white70, fontSize: 16)),
),
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: SizedBox(
height: 56,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.greenColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
onPressed: onAccept,
child: const Text('قبول الطلب',
style: TextStyle(
color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold)),
),
),
),
],
),
);
}
}