26-1-20/1
This commit is contained in:
@@ -3,58 +3,65 @@ import 'package:sefer_driver/views/widgets/my_textField.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart'; // Import this for formatting
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/rate/rate_conroller.dart';
|
||||
|
||||
// Changed: تم إعادة بناء الصفحة بالكامل لتحسين التصميم وتجربة المستخدم
|
||||
class RatePassenger extends StatelessWidget {
|
||||
final RateController controller = Get.put(RateController());
|
||||
|
||||
// Format: 1,234.5
|
||||
final NumberFormat currencyFormatter = NumberFormat("#,##0.0", "en_US");
|
||||
|
||||
RatePassenger({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// New: استخدام Scaffold القياسي لهيكل أكثر قوة ومرونة
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text('Rate Passenger'.tr),
|
||||
title: Text('Trip Completed'.tr,
|
||||
style: const TextStyle(color: Colors.black)),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false, // New: إزالة سهم الرجوع
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
elevation: 0,
|
||||
),
|
||||
// New: استخدام GetBuilder على مستوى الجسم لضمان تحديث الواجهة
|
||||
body: GetBuilder<RateController>(
|
||||
builder: (controller) {
|
||||
// New: استخدام SingleChildScrollView لتجنب مشاكل الـ overflow عند ظهور لوحة المفاتيح
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// New: استدعاء ودجت منفصلة لكل قسم لزيادة التنظيم
|
||||
_buildPriceSummaryCard(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// New: قسم المحفظة يظهر فقط إذا لم يتم التحقق منه
|
||||
if (controller.walletChecked != 'true')
|
||||
_buildWalletSection(context, controller),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildRatingSection(context, controller),
|
||||
// 1. The HERO Section: Big Price Display
|
||||
_buildHeroPriceDisplay(context),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
MyElevatedButton(
|
||||
title: 'Submit rating'.tr,
|
||||
onPressed: () => controller.addRateToPassenger(),
|
||||
// New: جعل الزر يأخذ العرض الكامل لمزيد من الوضوح
|
||||
// isFullWidth: true,
|
||||
// 2. Wallet Section (Conditional)
|
||||
if (controller.walletChecked != 'true')
|
||||
_buildWalletSection(context, controller),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 3. Rating Section
|
||||
_buildRatingSection(context, controller),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// 4. Submit Button
|
||||
SizedBox(
|
||||
height: 55,
|
||||
child: MyElevatedButton(
|
||||
title: 'Finish & Submit'.tr,
|
||||
onPressed: () => controller.addRateToPassenger(),
|
||||
// isFullWidth: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -65,85 +72,96 @@ class RatePassenger extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// New: ودجت منفصلة لعرض بطاقة ملخص السعر
|
||||
Widget _buildPriceSummaryCard(
|
||||
BuildContext context, RateController controller) {
|
||||
final MapDriverController mapController = Get.find<MapDriverController>();
|
||||
final double originalPrice =
|
||||
double.tryParse(controller.price.toString()) ?? 0.0;
|
||||
final double priceAfterDiscount = originalPrice - (originalPrice * 0.12);
|
||||
// --- WIDGETS ---
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'${'Trip Summary with'.tr} ${mapController.passengerName}',
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
Widget _buildHeroPriceDisplay(BuildContext context) {
|
||||
final MapDriverController mapController = Get.find<MapDriverController>();
|
||||
|
||||
// Parse the string to double to format it correctly
|
||||
double amount =
|
||||
double.tryParse(mapController.paymentAmount.toString()) ?? 0.0;
|
||||
String formattedAmount = currencyFormatter.format(amount);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor, // Use your brand color or a dark color
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Collect Cash'.tr.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
const Divider(height: 24, thickness: 1),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Original Fare'.tr, style: AppStyle.title),
|
||||
Text(
|
||||
priceAfterDiscount.toStringAsFixed(2),
|
||||
style: AppStyle.number.copyWith(
|
||||
fontSize: 16,
|
||||
color: AppColor.redColor,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Currency Symbol (Small)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'SYP'.tr, // Replace with your local currency symbol if needed
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Your Earnings'.tr,
|
||||
style:
|
||||
AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.greenColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColor.greenColor),
|
||||
),
|
||||
child: Text(
|
||||
mapController.paymentAmount,
|
||||
style: AppStyle.number
|
||||
.copyWith(color: AppColor.greenColor, fontSize: 20),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// The Price (Huge)
|
||||
Text(
|
||||
formattedAmount,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 56, // Very Large Font
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Exclusive offers and discounts always with the Sefer app'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.redColor, fontSize: 13),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
'Passenger: ${mapController.passengerName}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// New: ودجت منفصلة لقسم الدفع عبر المحفظة
|
||||
Widget _buildWalletSection(BuildContext context, RateController controller) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: controller.ispassengerWantWalletFromDriver
|
||||
@@ -154,37 +172,61 @@ class RatePassenger extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// New: واجهة سؤال استخدام المحفظة
|
||||
Widget _buildWalletQuery(RateController controller) {
|
||||
return Column(
|
||||
key: const ValueKey('walletQuery'),
|
||||
children: [
|
||||
Text(
|
||||
"Would the passenger like to settle the remaining fare using their wallet?"
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.account_balance_wallet,
|
||||
color: Colors.orange),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Pay remaining to Wallet?".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: 'No'.tr,
|
||||
onPressed: () {
|
||||
// يمكنك هنا تحديد ما يحدث عند الضغط على "لا"
|
||||
// حاليًا، ستبقى الواجهة كما هي أو يمكنك إخفاؤها
|
||||
},
|
||||
kolor: AppColor.redColor,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {}, // Optional logic
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.grey,
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Text('No'.tr),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
title: 'Yes, Pay'.tr,
|
||||
onPressed: () {
|
||||
controller.passengerWantPay();
|
||||
},
|
||||
child: ElevatedButton(
|
||||
onPressed: () => controller.passengerWantPay(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Text('Yes, Pay'.tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -193,137 +235,94 @@ class RatePassenger extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// New: واجهة إدخال المبلغ المدفوع
|
||||
Widget _buildAmountInput(RateController controller) {
|
||||
return Column(
|
||||
key: const ValueKey('amountInput'),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"How much Passenger pay?".tr,
|
||||
style: AppStyle.title,
|
||||
"Enter Amount Paid".tr,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.passengerPayAmount,
|
||||
label: "Passenger paid amount".tr,
|
||||
label: "Amount".tr,
|
||||
hint: "0.00",
|
||||
type: const TextInputType.numberWithOptions(decimal: true),
|
||||
// Suggestion: Add a suffix icon for currency if available in your widget
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
MyElevatedButton(
|
||||
title: "Add to Passenger Wallet".tr,
|
||||
// isFullWidth: true,
|
||||
onPressed: () {
|
||||
controller.addPassengerWallet();
|
||||
},
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => controller.addPassengerWallet(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Text("Confirm Payment".tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// New: ودجت منفصلة لقسم التقييم وكتابة الملاحظات
|
||||
Widget _buildRatingSection(BuildContext context, RateController controller) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('How was the passenger?'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
RatingBar.builder(
|
||||
initialRating: 0,
|
||||
itemCount: 5,
|
||||
itemSize: 40,
|
||||
itemPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
itemBuilder: (context, index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return const Icon(Icons.sentiment_very_dissatisfied,
|
||||
color: Colors.red);
|
||||
case 1:
|
||||
return const Icon(Icons.sentiment_dissatisfied,
|
||||
color: Colors.redAccent);
|
||||
case 2:
|
||||
return const Icon(Icons.sentiment_neutral,
|
||||
color: Colors.amber);
|
||||
case 3:
|
||||
return const Icon(Icons.sentiment_satisfied,
|
||||
color: Colors.lightGreen);
|
||||
case 4:
|
||||
return const Icon(Icons.sentiment_very_satisfied,
|
||||
color: Colors.green);
|
||||
default:
|
||||
return const Icon(Icons.sentiment_neutral,
|
||||
color: Colors.amber);
|
||||
}
|
||||
},
|
||||
onRatingUpdate: (rating) {
|
||||
controller.selectRateItem(rating);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
maxLines: 4,
|
||||
minLines: 2,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: controller.comment,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Add a comment (optional)'.tr,
|
||||
hintText: 'Type something...'.tr,
|
||||
prefixIcon: const Icon(Icons.rate_review_outlined),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// New: إضافة isFullWidth إلى MyElevatedButton لتسهيل التحكم في العرض
|
||||
// تأكد من تحديث ملف elevated_btn.dart بهذا التغيير
|
||||
/*
|
||||
class MyElevatedButton extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
final Color? kolor;
|
||||
final bool isFullWidth; // New property
|
||||
|
||||
const MyElevatedButton({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
this.kolor,
|
||||
this.isFullWidth = false, // Default value
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: isFullWidth ? double.infinity : null, // Apply width
|
||||
height: 50, // Standard height
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kolor ?? AppColor.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Rate Passenger'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textStyle: AppStyle.title.copyWith(color: Colors.white),
|
||||
),
|
||||
child: Text(title, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
RatingBar.builder(
|
||||
initialRating: 0,
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
allowHalfRating: false,
|
||||
itemCount: 5,
|
||||
itemSize: 45, // Large stars
|
||||
itemPadding: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||
itemBuilder: (context, _) => const Icon(
|
||||
Icons.star_rounded,
|
||||
color: Colors.amber,
|
||||
),
|
||||
onRatingUpdate: (rating) {
|
||||
controller.selectRateItem(rating);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Simplified comment box
|
||||
TextField(
|
||||
controller: controller.comment,
|
||||
maxLines: 2,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Any comments about the passenger?'.tr,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:sefer_driver/views/auth/captin/contact_us_page.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -18,20 +16,45 @@ import '../../../constant/style.dart';
|
||||
import '../../../controller/auth/apple_sigin.dart';
|
||||
import '../../../controller/auth/captin/login_captin_controller.dart';
|
||||
import '../../../controller/auth/google_sign.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../controller/functions/overlay_permisssion.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../country_widget.dart';
|
||||
import 'otp_page.dart';
|
||||
import 'contact_us_page.dart';
|
||||
import 'otp_page.dart'; // تأكد من وجود هذا الملف لديك
|
||||
|
||||
class LoginCaptin extends StatelessWidget {
|
||||
class LoginCaptin extends StatefulWidget {
|
||||
const LoginCaptin({super.key});
|
||||
|
||||
@override
|
||||
State<LoginCaptin> createState() => _LoginCaptinState();
|
||||
}
|
||||
|
||||
class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||
final AuthController authController = Get.put(AuthController());
|
||||
final LoginDriverController controller = Get.put(LoginDriverController());
|
||||
|
||||
LoginCaptin({super.key});
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// فحص الإذن عند فتح الصفحة مباشرة
|
||||
controller.getLocationPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
// التحقق عند العودة من الإعدادات
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
controller.getLocationPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -49,289 +72,25 @@ class LoginCaptin extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Determines which UI to show based on the driver's progress (agreement, permissions, login).
|
||||
Widget _buildBodyContent(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
// 1. صفحة الموافقة على الشروط
|
||||
if (box.read(BoxName.agreeTerms) != 'agreed') {
|
||||
return _buildAgreementPage(context, controller);
|
||||
}
|
||||
// if (box.read(BoxName.countryCode) == null) {
|
||||
// return CountryPicker(); // Assumed to be a full-screen widget
|
||||
// }
|
||||
|
||||
// 2. صفحة إذن الموقع
|
||||
if (box.read(BoxName.locationPermission) != 'true') {
|
||||
return _buildLocationPermissionPage(context, controller);
|
||||
}
|
||||
// Once all permissions are granted, show the main login UI
|
||||
|
||||
// 3. صفحة تسجيل الدخول (رقم الهاتف)
|
||||
return PhoneNumberScreen();
|
||||
}
|
||||
|
||||
/// Redesigned UI for the main login screen.
|
||||
Widget _buildLoginUI(BuildContext context, LoginDriverController controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Image.asset('assets/images/logo.gif', height: 120, width: 120),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Driver Portal'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2.copyWith(fontSize: 28),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Sign in to start your journey'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Conditional UI based on the controller state
|
||||
if (controller.isGoogleDashOpen)
|
||||
_buildManualLoginForm(context, controller, isRegistration: true)
|
||||
else if (Platform.isIOS && controller.isTest == 0)
|
||||
_buildManualLoginForm(context, controller, isRegistration: false)
|
||||
else
|
||||
_buildSocialLoginOptions(context, controller),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.to(() => ContactUsPage()),
|
||||
child: Text(
|
||||
'Need help? Contact Us'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.blueColor,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the social login buttons (Google, Apple, and manual option).
|
||||
Widget _buildSocialLoginOptions(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Sign in with a provider for easy access'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSocialButton(
|
||||
text: 'Sign In with Google'.tr,
|
||||
icon: FontAwesome.google,
|
||||
backgroundColor: AppColor.redColor,
|
||||
onPressed: () async {
|
||||
GoogleSignInHelper().signInFromLogin();
|
||||
},
|
||||
),
|
||||
if (Platform.isIOS) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSocialButton(
|
||||
text: 'Sign in with Apple'.tr,
|
||||
icon: Icons.apple,
|
||||
backgroundColor: Colors.black,
|
||||
onPressed: () async {
|
||||
User? user = await authController.signInWithApple();
|
||||
if (user != null) {
|
||||
box.write(BoxName.emailDriver, user.email.toString());
|
||||
box.write(BoxName.driverID, user.uid);
|
||||
controller.loginWithGoogleCredential(
|
||||
user.uid,
|
||||
user.email.toString(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Or'.tr, style: AppStyle.subtitle),
|
||||
),
|
||||
const Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
MyElevatedButton(
|
||||
title: 'Create Account with Email'.tr,
|
||||
onPressed: () => controller.changeGoogleDashOpen(),
|
||||
kolor: AppColor.blueColor, // Using 'kolor' as in your widget
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the form for manual email/password login or registration.
|
||||
Widget _buildManualLoginForm(
|
||||
BuildContext context, LoginDriverController controller,
|
||||
{required bool isRegistration}) {
|
||||
return Card(
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
isRegistration ? 'Create Driver Account'.tr : 'Driver Login'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTextFormField(
|
||||
controller: controller.emailController,
|
||||
labelText: 'Email'.tr,
|
||||
hintText: 'Enter your email'.tr,
|
||||
prefixIcon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || !GetUtils.isEmail(value)) {
|
||||
return 'Please enter a valid email'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
GetBuilder<LoginDriverController>(
|
||||
id: 'passwordVisibility', // ID to only rebuild this widget
|
||||
builder: (_) => _buildTextFormField(
|
||||
controller: controller.passwordController,
|
||||
labelText: 'Password'.tr,
|
||||
hintText: 'Enter your password'.tr,
|
||||
prefixIcon: Icons.lock_outline,
|
||||
obscureText: controller.isPasswordHidden,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordHidden
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
onPressed: () => controller.togglePasswordVisibility(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 6) {
|
||||
return 'Password must be at least 6 characters'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
controller.isloading
|
||||
? const Center(child: MyCircularProgressIndicator())
|
||||
: MyElevatedButton(
|
||||
title:
|
||||
(isRegistration ? 'Create Account'.tr : 'Login'.tr),
|
||||
onPressed: () {
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
if (isRegistration) {
|
||||
String email = controller.emailController.text;
|
||||
String uniqueId =
|
||||
controller.generateUniqueIdFromEmail(email);
|
||||
box.write(BoxName.driverID, uniqueId);
|
||||
box.write(BoxName.emailDriver, email);
|
||||
controller.loginUsingCredentialsWithoutGoogle(
|
||||
controller.passwordController.text,
|
||||
email,
|
||||
);
|
||||
} else {
|
||||
// This is the flow for iOS manual login
|
||||
controller.loginWithGoogleCredential(
|
||||
controller.passwordController.text,
|
||||
controller.emailController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
if (isRegistration) // Show back button only on the registration form
|
||||
TextButton(
|
||||
onPressed: () => controller.changeGoogleDashOpen(),
|
||||
child: Text(
|
||||
'Back to other sign-in options'.tr,
|
||||
style: TextStyle(color: AppColor.primaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A helper method to create styled TextFormFields.
|
||||
TextFormField _buildTextFormField({
|
||||
required TextEditingController controller,
|
||||
required String labelText,
|
||||
required String hintText,
|
||||
required IconData prefixIcon,
|
||||
required String? Function(String?) validator,
|
||||
bool obscureText = false,
|
||||
Widget? suffixIcon,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide:
|
||||
const BorderSide(color: AppColor.primaryColor, width: 2.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A helper for creating consistent social login buttons.
|
||||
Widget _buildSocialButton({
|
||||
required String text,
|
||||
required IconData icon,
|
||||
required Color backgroundColor,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return ElevatedButton.icon(
|
||||
icon: Icon(icon, color: Colors.white),
|
||||
label:
|
||||
Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Redesigned UI for the Terms and Conditions agreement page.
|
||||
// --- صفحة الشروط والأحكام ---
|
||||
Widget _buildAgreementPage(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
// This UI can be identical to the one in LoginPage for consistency.
|
||||
// I am reusing the improved design from the previous request.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
@@ -404,7 +163,7 @@ class LoginCaptin extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Redesigned UI for the Location Permission request page.
|
||||
// --- صفحة إذن الموقع ---
|
||||
Widget _buildLocationPermissionPage(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
return Padding(
|
||||
@@ -439,15 +198,33 @@ class LoginCaptin extends StatelessWidget {
|
||||
const SizedBox(height: 40),
|
||||
MyElevatedButton(
|
||||
title: "Allow Location Access".tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () async {
|
||||
Get.put(LoginDriverController());
|
||||
await getLocationPermission(); // Assumes this function handles the request logic
|
||||
if (await Permission.location.isGranted) {
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
controller.update(); // Re-check conditions
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
controller.update();
|
||||
// 1. طلب إذن الموقع العادي (أثناء الاستخدام) أولاً
|
||||
var status = await Permission.location.request();
|
||||
|
||||
if (status.isGranted) {
|
||||
// 2. إذا كنت تحتاج "طوال الوقت" (Background)، اطلبه الآن بشكل منفصل
|
||||
// ملاحظة: في أندرويد 11+ سينقلك هذا لصفحة إعدادات خاصة
|
||||
var backgroundStatus =
|
||||
await Permission.locationAlways.request();
|
||||
|
||||
if (backgroundStatus.isGranted) {
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
controller.update();
|
||||
} else {
|
||||
// المستخدم وافق على "أثناء الاستخدام" فقط، يمكنك المشي في الحال
|
||||
// أو إجباره، حسب منطق تطبيقك (تطبيق سائق يفضل Always)
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
controller.update();
|
||||
}
|
||||
} else if (status.isPermanentlyDenied) {
|
||||
// إذا كانت الحالة مرفوضة نهائياً، يجب فتح الإعدادات
|
||||
openAppSettings();
|
||||
}
|
||||
},
|
||||
kolor: AppColor.greenColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
@@ -459,4 +236,258 @@ class LoginCaptin extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- واجهة تسجيل الدخول اليدوي/الاجتماعي (للاستخدام المستقبلي إذا لزم الأمر) ---
|
||||
Widget _buildLoginUI(BuildContext context, LoginDriverController controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Image.asset('assets/images/logo.gif', height: 120, width: 120),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Driver Portal'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2.copyWith(fontSize: 28),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Sign in to start your journey'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
if (controller.isGoogleDashOpen)
|
||||
_buildManualLoginForm(context, controller, isRegistration: true)
|
||||
else if (Platform.isIOS && controller.isTest == 0)
|
||||
_buildManualLoginForm(context, controller, isRegistration: false)
|
||||
else
|
||||
_buildSocialLoginOptions(context, controller),
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.to(() => ContactUsPage()),
|
||||
child: Text(
|
||||
'Need help? Contact Us'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.blueColor,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSocialLoginOptions(
|
||||
BuildContext context, LoginDriverController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Sign in with a provider for easy access'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSocialButton(
|
||||
text: 'Sign In with Google'.tr,
|
||||
icon: FontAwesome.google,
|
||||
backgroundColor: AppColor.redColor,
|
||||
onPressed: () async {
|
||||
GoogleSignInHelper().signInFromLogin();
|
||||
},
|
||||
),
|
||||
if (Platform.isIOS) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSocialButton(
|
||||
text: 'Sign in with Apple'.tr,
|
||||
icon: Icons.apple,
|
||||
backgroundColor: Colors.black,
|
||||
onPressed: () async {
|
||||
User? user = await authController.signInWithApple();
|
||||
if (user != null) {
|
||||
box.write(BoxName.emailDriver, user.email.toString());
|
||||
box.write(BoxName.driverID, user.uid);
|
||||
controller.loginWithGoogleCredential(
|
||||
user.uid,
|
||||
user.email.toString(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text('Or'.tr, style: AppStyle.subtitle),
|
||||
),
|
||||
const Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
MyElevatedButton(
|
||||
title: 'Create Account with Email'.tr,
|
||||
onPressed: () => controller.changeGoogleDashOpen(),
|
||||
kolor: AppColor.blueColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildManualLoginForm(
|
||||
BuildContext context, LoginDriverController controller,
|
||||
{required bool isRegistration}) {
|
||||
return Card(
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
isRegistration ? 'Create Driver Account'.tr : 'Driver Login'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTextFormField(
|
||||
controller: controller.emailController,
|
||||
labelText: 'Email'.tr,
|
||||
hintText: 'Enter your email'.tr,
|
||||
prefixIcon: Icons.email_outlined,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || !GetUtils.isEmail(value)) {
|
||||
return 'Please enter a valid email'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
GetBuilder<LoginDriverController>(
|
||||
id: 'passwordVisibility',
|
||||
builder: (_) => _buildTextFormField(
|
||||
controller: controller.passwordController,
|
||||
labelText: 'Password'.tr,
|
||||
hintText: 'Enter your password'.tr,
|
||||
prefixIcon: Icons.lock_outline,
|
||||
obscureText: controller.isPasswordHidden,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
controller.isPasswordHidden
|
||||
? Icons.visibility_off
|
||||
: Icons.visibility,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
onPressed: () => controller.togglePasswordVisibility(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 6) {
|
||||
return 'Password must be at least 6 characters'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
controller.isloading
|
||||
? const Center(child: MyCircularProgressIndicator())
|
||||
: MyElevatedButton(
|
||||
title:
|
||||
(isRegistration ? 'Create Account'.tr : 'Login'.tr),
|
||||
onPressed: () {
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
if (isRegistration) {
|
||||
String email = controller.emailController.text;
|
||||
String uniqueId =
|
||||
controller.generateUniqueIdFromEmail(email);
|
||||
box.write(BoxName.driverID, uniqueId);
|
||||
box.write(BoxName.emailDriver, email);
|
||||
controller.loginUsingCredentialsWithoutGoogle(
|
||||
controller.passwordController.text,
|
||||
email,
|
||||
);
|
||||
} else {
|
||||
controller.loginWithGoogleCredential(
|
||||
controller.passwordController.text,
|
||||
controller.emailController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
if (isRegistration)
|
||||
TextButton(
|
||||
onPressed: () => controller.changeGoogleDashOpen(),
|
||||
child: Text(
|
||||
'Back to other sign-in options'.tr,
|
||||
style: TextStyle(color: AppColor.primaryColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextFormField _buildTextFormField({
|
||||
required TextEditingController controller,
|
||||
required String labelText,
|
||||
required String hintText,
|
||||
required IconData prefixIcon,
|
||||
required String? Function(String?) validator,
|
||||
bool obscureText = false,
|
||||
Widget? suffixIcon,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide:
|
||||
const BorderSide(color: AppColor.primaryColor, width: 2.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSocialButton({
|
||||
required String text,
|
||||
required IconData icon,
|
||||
required Color backgroundColor,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return ElevatedButton.icon(
|
||||
icon: Icon(icon, color: Colors.white),
|
||||
label:
|
||||
Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,52 @@ class RegistrationView extends StatelessWidget {
|
||||
style:
|
||||
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
// ============================================================
|
||||
// 1. القائمة المنسدلة لتصنيف المركبة (سيارة، دراجة، فان)
|
||||
// ============================================================
|
||||
DropdownButtonFormField<int>(
|
||||
value: c.selectedVehicleCategoryId,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Vehicle Category'.tr, // ترجمة: تصنيف المركبة
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.directions_car),
|
||||
),
|
||||
items: c.vehicleCategoryOptions.map((item) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: item['id'],
|
||||
child: Text(item['name']),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
c.selectedVehicleCategoryId = val;
|
||||
c.update(); // تحديث الواجهة إذا لزم الأمر
|
||||
},
|
||||
validator: (v) => (v == null) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// ============================================================
|
||||
// 2. القائمة المنسدلة لنوع الوقود (بنزين، كهرباء...)
|
||||
// ============================================================
|
||||
DropdownButtonFormField<int>(
|
||||
value: c.selectedFuelTypeId,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Fuel Type'.tr, // ترجمة: نوع الوقود
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.local_gas_station),
|
||||
),
|
||||
items: c.fuelTypeOptions.map((item) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: item['id'],
|
||||
child: Text(item['name']),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
c.selectedFuelTypeId = val;
|
||||
c.update();
|
||||
},
|
||||
validator: (v) => (v == null) ? 'Required field'.tr : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.carPlateController,
|
||||
decoration: InputDecoration(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/functions/location_controller.dart';
|
||||
import '../../../main.dart';
|
||||
@@ -15,6 +15,7 @@ import 'mapDriverWidgets/google_driver_map_page.dart';
|
||||
import 'mapDriverWidgets/google_map_app.dart';
|
||||
import 'mapDriverWidgets/passenger_info_window.dart';
|
||||
import 'mapDriverWidgets/sos_connect.dart';
|
||||
import 'mapDriverWidgets/sped_circle.dart';
|
||||
|
||||
class PassengerLocationMapPage extends StatelessWidget {
|
||||
PassengerLocationMapPage({super.key});
|
||||
@@ -22,26 +23,23 @@ class PassengerLocationMapPage extends StatelessWidget {
|
||||
final MapDriverController mapDriverController =
|
||||
Get.put(MapDriverController());
|
||||
|
||||
// Helper function to show exit confirmation dialog
|
||||
// دالة ديالوج الخروج
|
||||
Future<bool> showExitDialog() async {
|
||||
bool? result = await Get.defaultDialog(
|
||||
title: "Warning".tr,
|
||||
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||
middleText:
|
||||
"You are in an active ride. Leaving this screen might stop tracking. Are you sure you want to exit?"
|
||||
.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
"Active ride in progress. Leaving might stop tracking. Exit?".tr,
|
||||
barrierDismissible: false,
|
||||
radius: 15,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Stay'.tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () => Get.back(result: false), // Return false (Don't pop)
|
||||
),
|
||||
title: 'Stay'.tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () => Get.back(result: false)),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Exit'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () => Get.back(result: true), // Return true (Allow pop)
|
||||
),
|
||||
title: 'Exit'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () => Get.back(result: true)),
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
@@ -52,202 +50,205 @@ class PassengerLocationMapPage extends StatelessWidget {
|
||||
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
|
||||
mapDriverController.argumentLoading();
|
||||
mapDriverController.startTimerToShowPassengerInfoWindowFromDriver();
|
||||
// 2. فرض التحديث لكل المعرفات (IDs) لضمان ظهورها
|
||||
// لأن argumentLoading قد تستدعي update() العادية التي لا تؤثر على هؤلاء
|
||||
mapDriverController
|
||||
.update(['PassengerInfo', 'DriverEndBar', 'SosConnect']);
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Added PopScope to intercept back button
|
||||
return PopScope(
|
||||
canPop: false, // Prevents immediate popping
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) {
|
||||
return;
|
||||
}
|
||||
// Show dialog
|
||||
if (didPop) return;
|
||||
final shouldExit = await showExitDialog();
|
||||
if (shouldExit) {
|
||||
Get.back(); // Manually pop if confirmed
|
||||
}
|
||||
if (shouldExit) Get.back();
|
||||
},
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
children: [
|
||||
// 1. Map
|
||||
GoogleDriverMap(locationController: locationController),
|
||||
// 1. الخريطة (الخلفية)
|
||||
Positioned.fill(
|
||||
child: GoogleDriverMap(locationController: locationController)),
|
||||
|
||||
// 2. Instructions
|
||||
const InstructionsOfRoads(),
|
||||
// 2. واجهة المستخدم (فوق الخريطة)
|
||||
SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// أ) زر الإلغاء (أعلى اليسار)
|
||||
CancelWidget(mapDriverController: mapDriverController),
|
||||
|
||||
// 3. Passenger Info
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: PassengerInfoWindow(),
|
||||
// ب) شريط إنهاء الرحلة (أعلى الوسط)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(child: driverEndRideBar())),
|
||||
|
||||
// ج) شريط التعليمات الملاحية (الأسفل)
|
||||
const InstructionsOfRoads(),
|
||||
|
||||
// د) نافذة معلومات الراكب (تعلو التعليمات ديناميكياً)
|
||||
const PassengerInfoWindow(),
|
||||
// SpeedCircle(),
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 20, // أو أي مسافة تناسبك
|
||||
child: GetBuilder<MapDriverController>(
|
||||
// id: 'SosConnect', // لتحديث الزر عند بدء الرحلة
|
||||
builder: (controller) {
|
||||
// حساب الهوامش ديناميكياً لرفع الأزرار فوق النوافذ السفلية
|
||||
double bottomPadding = 0;
|
||||
if (controller.currentInstruction.isNotEmpty)
|
||||
bottomPadding += 120;
|
||||
if (controller.isPassengerInfoWindow)
|
||||
bottomPadding += 220;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: EdgeInsets.only(bottom: bottomPadding),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SosConnect(), // ويدجت نظيفة
|
||||
const SizedBox(height: 12),
|
||||
const GoogleMapApp(), // ويدجت نظيفة
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 4. Cancel Widget
|
||||
CancelWidget(mapDriverController: mapDriverController),
|
||||
|
||||
// 5. End Ride Bar
|
||||
driverEndRideBar(),
|
||||
|
||||
// 6. SOS
|
||||
SosConnect(),
|
||||
|
||||
// 7. Speed
|
||||
speedCircle(),
|
||||
|
||||
// 8. External Map
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
right: 10,
|
||||
child: GoogleMapApp(),
|
||||
),
|
||||
|
||||
// 9. Prices Window
|
||||
// 3. النوافذ المنبثقة (Overlay)
|
||||
const PricesWindow(),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ... The rest of your widgets (InstructionsOfRoads, CancelWidget, etc.) remain unchanged ...
|
||||
// ... Keep the code below exactly as you had it in the previous snippet ...
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. ويدجت شريط التعليمات (InstructionsOfRoads)
|
||||
// ---------------------------------------------------------------------------
|
||||
class InstructionsOfRoads extends StatelessWidget {
|
||||
const InstructionsOfRoads({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 10,
|
||||
left: MediaQuery.of(context).size.width * 0.15,
|
||||
right: MediaQuery.of(context).size.width * 0.15,
|
||||
child: GetBuilder<MapDriverController>(
|
||||
builder: (controller) => controller.currentInstruction.isNotEmpty
|
||||
? AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.directions, color: AppColor.primaryColor),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.currentInstruction,
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
controller.toggleTts();
|
||||
},
|
||||
child: Icon(
|
||||
controller.isTtsEnabled
|
||||
? Icons.volume_up
|
||||
: Icons.volume_off,
|
||||
color: controller.isTtsEnabled
|
||||
? AppColor.greenColor
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CancelWidget extends StatelessWidget {
|
||||
const CancelWidget({
|
||||
super.key,
|
||||
required this.mapDriverController,
|
||||
});
|
||||
|
||||
final MapDriverController mapDriverController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 70,
|
||||
left: 10,
|
||||
bottom: 20,
|
||||
left: 15,
|
||||
right: 15,
|
||||
child: GetBuilder<MapDriverController>(
|
||||
builder: (controller) {
|
||||
if (controller.isRideFinished) return const SizedBox.shrink();
|
||||
// إخفاء الشريط إذا لم يكن هناك تعليمات
|
||||
if (controller.currentInstruction.isEmpty) return const SizedBox();
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.defaultDialog(
|
||||
title: "Are you sure you want to cancel this trip?".tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Text("Why do you want to cancel this trip?".tr),
|
||||
Form(
|
||||
key: mapDriverController.formKeyCancel,
|
||||
child: MyTextForm(
|
||||
controller: mapDriverController.cancelTripCotroller,
|
||||
label: "Write the reason for canceling the trip".tr,
|
||||
hint: "Write the reason for canceling the trip".tr,
|
||||
type: TextInputType.name,
|
||||
))
|
||||
],
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
builder: (context, value, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 50 * (1 - value)), // حركة انزلاق
|
||||
child: Opacity(
|
||||
opacity: value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1F1F1F)
|
||||
.withOpacity(0.95), // خلفية داكنة
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5)),
|
||||
],
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// أيقونة الاتجاه
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.turn_right_rounded,
|
||||
color: Colors.white, size: 24),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// نص التعليمات
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"NEXT STEP".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade500,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
controller.currentInstruction,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// فاصل عمودي
|
||||
Container(
|
||||
width: 1,
|
||||
height: 30,
|
||||
color: Colors.white12,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10)),
|
||||
|
||||
// زر التحكم بالصوت
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => controller.toggleTts(),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
controller.isTtsEnabled
|
||||
? Icons.volume_up_rounded
|
||||
: Icons.volume_off_rounded,
|
||||
color: controller.isTtsEnabled
|
||||
? AppColor.greenColor
|
||||
: Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () async {
|
||||
await mapDriverController
|
||||
.cancelTripFromDriverAfterApplied();
|
||||
Get.back();
|
||||
}),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'No'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
}));
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.clear,
|
||||
size: 30,
|
||||
color: AppColor.redColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -255,56 +256,193 @@ class CancelWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class PricesWindow extends StatelessWidget {
|
||||
const PricesWindow({
|
||||
super.key,
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. ويدجت زر الإلغاء (CancelWidget) - كامل
|
||||
// ---------------------------------------------------------------------------
|
||||
class CancelWidget extends StatelessWidget {
|
||||
const CancelWidget({super.key, required this.mapDriverController});
|
||||
final MapDriverController mapDriverController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(builder: (mapDriverController) {
|
||||
return mapDriverController.isPriceWindow
|
||||
? Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
child: Center(
|
||||
return Positioned(
|
||||
top: 10,
|
||||
left: 15,
|
||||
child: GetBuilder<MapDriverController>(builder: (controller) {
|
||||
// نخفي الزر إذا انتهت الرحلة
|
||||
if (controller.isRideFinished) return const SizedBox();
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8)
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
onTap: () => _showCancelDialog(context, controller),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Icon(Icons.close_rounded,
|
||||
color: AppColor.redColor, size: 26),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCancelDialog(BuildContext context, MapDriverController controller) {
|
||||
Get.defaultDialog(
|
||||
title: "Cancel Trip?".tr,
|
||||
titleStyle: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
radius: 16,
|
||||
content: Column(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded,
|
||||
size: 50, color: Colors.orangeAccent),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Please tell us why you want to cancel.".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Form(
|
||||
key: controller.formKeyCancel,
|
||||
child: MyTextForm(
|
||||
controller: controller.cancelTripCotroller,
|
||||
label: "Reason".tr,
|
||||
hint: "Write your reason...".tr,
|
||||
type: TextInputType.text,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: SizedBox(
|
||||
width: 100,
|
||||
child: MyElevatedButton(
|
||||
title: 'Confirm'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () async {
|
||||
// استدعاء دالة الإلغاء من الكنترولر
|
||||
await controller.cancelTripFromDriverAfterApplied();
|
||||
// Get.back(); // عادة موجودة داخل الدالة في الكنترولر
|
||||
},
|
||||
),
|
||||
),
|
||||
cancel: SizedBox(
|
||||
width: 100,
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Back'.tr, style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. ويدجت نافذة الأسعار (PricesWindow) - كامل
|
||||
// ---------------------------------------------------------------------------
|
||||
class PricesWindow extends StatelessWidget {
|
||||
const PricesWindow({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(builder: (controller) {
|
||||
// إخفاء إذا لم تكن مفعلة
|
||||
if (!controller.isPriceWindow) return const SizedBox();
|
||||
|
||||
return Container(
|
||||
color: Colors.black.withOpacity(0.6), // خلفية معتمة
|
||||
child: Center(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.8, end: 1.0),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
builder: (context, scale, child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
width: Get.width * 0.8,
|
||||
padding: const EdgeInsets.all(24),
|
||||
width: Get.width * 0.85,
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.check_circle_rounded,
|
||||
color: AppColor.primaryColor, size: 50),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Total Price is '.tr,
|
||||
style: AppStyle.headTitle2,
|
||||
textAlign: TextAlign.center,
|
||||
'Total Price'.tr,
|
||||
style: AppStyle.headTitle2
|
||||
.copyWith(fontSize: 18, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'${mapDriverController.totalPricePassenger} ${'\$'.tr}',
|
||||
'${controller.totalCost} ${'\$'.tr}',
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: AppColor.primaryColor, fontSize: 36),
|
||||
color: Colors.black87,
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
const SizedBox(height: 30),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: MyElevatedButton(
|
||||
title: 'Collect Payment'.tr,
|
||||
kolor: AppColor.primaryColor,
|
||||
onPressed: () {
|
||||
// الذهاب لصفحة التقييم
|
||||
Get.to(() => RatePassenger(), arguments: {
|
||||
'rideId': controller.rideId,
|
||||
'passengerId': controller.passengerId,
|
||||
'driverId': controller.driverId,
|
||||
'price': controller.paymentAmount,
|
||||
'walletChecked': controller.walletChecked
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'ok'.tr,
|
||||
onPressed: () =>
|
||||
Get.to(() => RatePassenger(), arguments: {
|
||||
'rideId': mapDriverController.rideId,
|
||||
'passengerId': mapDriverController.passengerId,
|
||||
'driverId': mapDriverController.driverId
|
||||
}))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:sefer_driver/views/home/my_wallet/walet_captain.dart';
|
||||
import 'package:sefer_driver/views/home/profile/profile_captain.dart';
|
||||
import 'package:sefer_driver/views/notification/notification_captain.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../About Us/video_page.dart';
|
||||
import '../assurance_health_page.dart';
|
||||
import '../maintain_center_page.dart';
|
||||
@@ -227,12 +228,57 @@ class _DrawerItemTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
// --- ويدجت محسنة للجزء العلوي من القائمة ---
|
||||
// ... (الاستيرادات السابقة تبقى كما هي)
|
||||
|
||||
// --- تم تعديل UserHeader لإضافة التحقق من الصورة ---
|
||||
class UserHeader extends StatelessWidget {
|
||||
UserHeader({super.key});
|
||||
final ImageController imageController = Get.find<ImageController>();
|
||||
final HomeCaptainController homeCaptainController =
|
||||
Get.find<HomeCaptainController>();
|
||||
|
||||
// دالة لإظهار التنبيه
|
||||
void _showUploadPhotoDialog(
|
||||
BuildContext context, ImageController controller) {
|
||||
// نستخدم addPostFrameCallback لضمان عدم ظهور الخطأ أثناء بناء الواجهة
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// نتأكد ألا يكون هناك dialog مفتوح بالفعل لتجنب التكرار
|
||||
if (Get.isDialogOpen == true) return;
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "Profile Photo Required".tr,
|
||||
titleStyle:
|
||||
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
middleText:
|
||||
"Please upload a clear photo of your face to be identified by passengers."
|
||||
.tr,
|
||||
barrierDismissible: false, // منع الإغلاق بالضغط خارج النافذة
|
||||
radius: 15,
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
confirm: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Get.back(); // إغلاق النافذة الحالية
|
||||
// فتح الكاميرا فوراً
|
||||
controller.choosImagePicture(
|
||||
AppLink.uploadImagePortrate, 'portrait');
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt, color: Colors.white),
|
||||
label: Text("Take Photo Now".tr,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor
|
||||
.primaryColor, // تأكد من وجود هذا اللون أو استبدله بـ Colors.blue
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text("Later".tr, style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UserAccountsDrawerHeader(
|
||||
@@ -262,8 +308,23 @@ class UserHeader extends StatelessWidget {
|
||||
child: controller.isloading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: CircleAvatar(
|
||||
// محاولة تحميل الصورة
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${box.read(BoxName.driverID)}.jpg'),
|
||||
|
||||
// [تعديل هام]: في حال فشل تحميل الصورة (غير موجودة)
|
||||
onBackgroundImageError: (exception, stackTrace) {
|
||||
// طباعة الخطأ في الكونسول للتوضيح
|
||||
debugPrint(
|
||||
"Profile image not found or error loading: $exception");
|
||||
// استدعاء نافذة التنبيه
|
||||
_showUploadPhotoDialog(context, controller);
|
||||
},
|
||||
|
||||
// أيقونة بديلة تظهر في الخلفية إذا لم تكن الصورة موجودة
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
child: const Icon(Icons.person,
|
||||
size: 40, color: Colors.white),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/info.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/functions/location_background_controller.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/functions/overlay_permisssion.dart';
|
||||
import '../../../../controller/functions/package_info.dart';
|
||||
@@ -44,29 +45,33 @@ class HomeCaptain extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
// Initial calls remain the same.
|
||||
// Get.put(HomeCaptainController());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
closeOverlayIfFound();
|
||||
checkForUpdate(context);
|
||||
getPermissionOverlay();
|
||||
showDriverGiftClaim(context);
|
||||
checkForAppliedRide(context);
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
print("🔥 HomeCaptain postFrameCallback started"); // Debug
|
||||
await closeOverlayIfFound();
|
||||
await checkForUpdate(context);
|
||||
await getPermissionOverlay();
|
||||
await showDriverGiftClaim(context);
|
||||
await checkForAppliedRide(context);
|
||||
print("✅ postFrameCallback completed");
|
||||
});
|
||||
// The stack is now even simpler.
|
||||
return Scaffold(
|
||||
appBar: const _HomeAppBar(),
|
||||
drawer: AppDrawer(),
|
||||
body: Stack(
|
||||
children: [
|
||||
// 1. The Map View is the base layer.
|
||||
const _MapView(),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// 1. The Map View is the base layer.
|
||||
const _MapView(),
|
||||
|
||||
// 2. The new floating "Status Pod" at the bottom.
|
||||
const _StatusPodOverlay(),
|
||||
FloatingActionButtons(),
|
||||
// This widget from the original code remains.
|
||||
leftMainMenuCaptainIcons(),
|
||||
],
|
||||
// 2. The new floating "Status Pod" at the bottom.
|
||||
const _StatusPodOverlay(),
|
||||
FloatingActionButtons(),
|
||||
// This widget from the original code remains.
|
||||
leftMainMenuCaptainIcons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -139,12 +144,12 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
tooltip: 'Change Map Type'.tr,
|
||||
onPressed: homeCaptainController.changeMapType,
|
||||
),
|
||||
_MapControlButton(
|
||||
iconColor: Colors.blue,
|
||||
icon: Icons.streetview_sharp,
|
||||
tooltip: 'Toggle Traffic'.tr,
|
||||
onPressed: homeCaptainController.changeMapTraffic,
|
||||
),
|
||||
// _MapControlButton(
|
||||
// iconColor: Colors.blue,
|
||||
// icon: Icons.streetview_sharp,
|
||||
// tooltip: 'Toggle Traffic'.tr,
|
||||
// onPressed: homeCaptainController.changeMapTraffic,
|
||||
// ),
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (controller) {
|
||||
return _MapControlButton(
|
||||
@@ -250,6 +255,7 @@ class _MapView extends StatelessWidget {
|
||||
// --- تم حذف onCameraMove الخاطئ ---
|
||||
// === إضافة الطبقة الحرارية هنا ===
|
||||
polygons: controller.heatmapPolygons,
|
||||
|
||||
// =
|
||||
markers: {
|
||||
Marker(
|
||||
@@ -346,9 +352,10 @@ class _MapView extends StatelessWidget {
|
||||
class _StatusPodOverlay extends StatelessWidget {
|
||||
const _StatusPodOverlay();
|
||||
|
||||
void _showDetailsDialog(BuildContext context) {
|
||||
void _showDetailsDialog(
|
||||
BuildContext context, HomeCaptainController controller) {
|
||||
Get.dialog(
|
||||
const _DriverDetailsDialog(),
|
||||
_DriverDetailsDialog(controller), // تمرير الكنترولر هنا
|
||||
barrierColor: Colors.black.withOpacity(0.3),
|
||||
);
|
||||
}
|
||||
@@ -361,7 +368,7 @@ class _StatusPodOverlay extends StatelessWidget {
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showDetailsDialog(context),
|
||||
onTap: () => _showDetailsDialog(context, homeCaptainController),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: BackdropFilter(
|
||||
@@ -435,11 +442,16 @@ class _StatusPodOverlay extends StatelessWidget {
|
||||
|
||||
/// 4. The Dialog that shows detailed driver stats.
|
||||
class _DriverDetailsDialog extends StatelessWidget {
|
||||
const _DriverDetailsDialog();
|
||||
// 1. إضافة متغير للكنترولر
|
||||
final HomeCaptainController controller;
|
||||
|
||||
// 2. تحديث البناء لاستقباله
|
||||
const _DriverDetailsDialog(this.controller);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final homeCaptainController = Get.find<HomeCaptainController>();
|
||||
// 3. حذف السطر الذي يسبب الخطأ: final homeCaptainController = Get.find...
|
||||
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: AlertDialog(
|
||||
@@ -463,27 +475,28 @@ class _DriverDetailsDialog extends StatelessWidget {
|
||||
icon: Entypo.wallet,
|
||||
color: AppColor.greenColor,
|
||||
label: 'Today'.tr,
|
||||
value: homeCaptainController.totalMoneyToday.toString(),
|
||||
// استخدام المتغير controller الذي تم تمريره
|
||||
value: controller.totalMoneyToday.toString(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatRow(
|
||||
icon: Entypo.wallet,
|
||||
color: AppColor.yellowColor,
|
||||
label: AppInformation.appName,
|
||||
value: homeCaptainController.totalMoneyInSEFER.toString(),
|
||||
value: controller.totalMoneyInSEFER.toString(),
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildDurationRow(
|
||||
icon: Icons.timer_outlined,
|
||||
label: 'Active Duration:'.tr,
|
||||
value: homeCaptainController.stringActiveDuration,
|
||||
value: controller.stringActiveDuration,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDurationRow(
|
||||
icon: Icons.access_time,
|
||||
label: 'Total Connection Duration:'.tr,
|
||||
value: homeCaptainController.totalDurationToday,
|
||||
value: controller.totalDurationToday,
|
||||
color: AppColor.accentColor,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
@@ -491,7 +504,7 @@ class _DriverDetailsDialog extends StatelessWidget {
|
||||
icon: Icons.star_border_rounded,
|
||||
color: AppColor.blueColor,
|
||||
label: 'Total Points'.tr,
|
||||
value: homeCaptainController.totalPoints.toString(),
|
||||
value: controller.totalPoints.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -508,6 +521,7 @@ class _DriverDetailsDialog extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ... بقية الدوال المساعدة (_buildStatRow, _buildDurationRow) تبقى كما هي ...
|
||||
Widget _buildStatRow(
|
||||
{required IconData icon,
|
||||
required Color color,
|
||||
|
||||
@@ -20,13 +20,13 @@ class ConnectWidget extends StatelessWidget {
|
||||
// Get.put(OrderRequestController());
|
||||
CaptainWalletController captainWalletController =
|
||||
Get.put(CaptainWalletController());
|
||||
|
||||
int refusedRidesToday = 0;
|
||||
captainWalletController.getCaptainWalletFromBuyPoints();
|
||||
return Center(
|
||||
child: GetBuilder<HomeCaptainController>(
|
||||
builder: (homeCaptainController) => double.parse(
|
||||
(captainWalletController.totalPoints)) <
|
||||
-30000
|
||||
-200
|
||||
? CupertinoButton(
|
||||
onPressed: () {
|
||||
Get.defaultDialog(
|
||||
@@ -34,7 +34,7 @@ class ConnectWidget extends StatelessWidget {
|
||||
barrierDismissible: false,
|
||||
title: double.parse(
|
||||
(captainWalletController.totalPoints)) <
|
||||
-30000
|
||||
-200
|
||||
? 'You dont have Points'.tr
|
||||
: 'You Are Stopped For this Day !'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
@@ -44,7 +44,7 @@ class ConnectWidget extends StatelessWidget {
|
||||
onPressed: () async {
|
||||
double.parse((captainWalletController
|
||||
.totalPoints)) <
|
||||
-30000
|
||||
-200
|
||||
? await Get.find<TextToSpeechController>()
|
||||
.speakText(
|
||||
'You must be recharge your Account'
|
||||
@@ -59,7 +59,7 @@ class ConnectWidget extends StatelessWidget {
|
||||
Text(
|
||||
double.parse((captainWalletController
|
||||
.totalPoints)) <
|
||||
-30000
|
||||
-200
|
||||
? 'You must be recharge your Account'.tr
|
||||
: 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
|
||||
.tr,
|
||||
@@ -69,7 +69,7 @@ class ConnectWidget extends StatelessWidget {
|
||||
),
|
||||
confirm: double.parse(
|
||||
(captainWalletController.totalPoints)) <
|
||||
-30000
|
||||
-200
|
||||
? MyElevatedButton(
|
||||
title: 'Recharge my Account'.tr,
|
||||
onPressed: () {
|
||||
|
||||
@@ -183,8 +183,13 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||
// child: Builder(builder: (context) {
|
||||
// return IconButton(
|
||||
// onPressed: () async {
|
||||
// Get.to(() => const PhoneNumberScreen());
|
||||
// // box.write(BoxName.statusDriverLocation, 'off');
|
||||
// NotificationService.sendNotification(
|
||||
// target: 'service', // الإرسال لجميع المشتركين في "service"
|
||||
// title: 'طلب خدمة جديد',
|
||||
// body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.',
|
||||
// isTopic: true,
|
||||
// category: 'new_service_request', // فئة توضح نوع الإشعار
|
||||
// );
|
||||
// },
|
||||
// icon: const Icon(
|
||||
// FontAwesome5.grin_tears,
|
||||
|
||||
@@ -1,265 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:slide_to_act/slide_to_act.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../widgets/elevated_btn.dart';
|
||||
|
||||
// Changed: إعادة تصميم كاملة للشريط ليصبح شريطًا علويًا عند بدء الرحلة
|
||||
// ملف: driver_end_ride_bar.dart
|
||||
|
||||
Widget driverEndRideBar() {
|
||||
// 1. Positioned هي الوالد المباشر (لأنها داخل Stack في الصفحة الرئيسية)
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
// 2. GetBuilder يكون في الداخل
|
||||
child: GetBuilder<MapDriverController>(
|
||||
builder: (controller) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
// 3. نستخدم التحريك (Translation) لإخفاء الشريط وإظهاره بدلاً من تغيير الـ top
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (controller) {
|
||||
// 🔥 فحص هل السعر ثابت للعرض
|
||||
final String carType = controller.carType;
|
||||
final bool isFixed = (carType == 'Speed' ||
|
||||
carType == 'Awfar' ||
|
||||
carType == 'Fixed Price');
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeOutBack,
|
||||
transform: Matrix4.translationValues(
|
||||
0, controller.isRideStarted ? 0 : -250, 0),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
elevation: 10,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.carType != 'Mishwar Vip')
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildInfoColumn(
|
||||
icon: Icons.social_distance,
|
||||
text: '${controller.distance} ${'KM'.tr}',
|
||||
label: 'Distance'.tr,
|
||||
),
|
||||
_buildInfoColumn(
|
||||
icon: Icons.timelapse,
|
||||
text: controller.hours > 1
|
||||
? '${controller.hours}h ${controller.minutes}m'
|
||||
: '${controller.minutes}m',
|
||||
label: 'Time'.tr,
|
||||
),
|
||||
_buildInfoColumn(
|
||||
icon: Icons.money_sharp,
|
||||
text:
|
||||
'${NumberFormat('#,##0').format(double.tryParse(controller.paymentAmount.toString()) ?? 0)} ${'SYP'.tr}',
|
||||
label: 'Price'.tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ... بقية الكود كما هو (الأزرار والمؤقت)
|
||||
if (controller.carType != 'Mishwar Vip')
|
||||
const Divider(height: 20),
|
||||
const _builtTimerAndCarType(),
|
||||
const SizedBox(height: 12),
|
||||
SlideAction(
|
||||
height: 55,
|
||||
borderRadius: 15,
|
||||
elevation: 4,
|
||||
text: 'Slide to End Trip'.tr,
|
||||
textStyle: AppStyle.title.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
outerColor: AppColor.redColor,
|
||||
innerColor: Colors.white,
|
||||
sliderButtonIcon: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: AppColor.redColor,
|
||||
size: 24,
|
||||
),
|
||||
sliderRotate: false,
|
||||
onSubmit: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
controller.finishRideFromDriver();
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// New: ودجت لعرض معلومات الرحلة في الشريط العلوي
|
||||
Widget _buildInfoColumn(
|
||||
{required IconData icon, required String text, required String label}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: AppColor.primaryColor),
|
||||
const SizedBox(height: 4),
|
||||
Text(text, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
Text(label,
|
||||
style:
|
||||
AppStyle.title.copyWith(color: Colors.grey[600], fontSize: 12)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Changed: تم تعديل تصميم ودجت عرض المؤقت ونوع السيارة
|
||||
class _builtTimerAndCarType extends StatelessWidget {
|
||||
const _builtTimerAndCarType();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// نستخدم GetBuilder هنا لضمان تحديث العداد في كل ثانية
|
||||
return GetBuilder<MapDriverController>(builder: (controller) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// -- نوع السيارة --
|
||||
Container(
|
||||
decoration:
|
||||
AppStyle.boxDecoration1.copyWith(color: Colors.grey[200]),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
controller.carType.tr,
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
||||
// -- مؤقت الرحلة --
|
||||
if (controller.carType != 'Comfort' &&
|
||||
controller.carType != 'Mishwar Vip' &&
|
||||
controller.carType != 'Lady') ...[
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
controller.remainingTimeTimerRideBegin < 60
|
||||
? AppColor.redColor.withOpacity(0.8)
|
||||
: AppColor.greenColor.withOpacity(0.8),
|
||||
controller.remainingTimeTimerRideBegin < 60
|
||||
? AppColor.redColor
|
||||
: AppColor.greenColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
backgroundColor: Colors.transparent,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white.withOpacity(0.2)),
|
||||
minHeight: 40,
|
||||
// تأكد من أن هذه القيمة بين 0.0 و 1.0 في الكونترولر
|
||||
value: controller.progressTimerRideBegin.toDouble(),
|
||||
),
|
||||
Text(
|
||||
controller.stringRemainingTimeRideBegin,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
0,
|
||||
controller.isRideStarted ? 0 : -300,
|
||||
0,
|
||||
), // Matrix4.translationValues مستخدمة للإزاحة [web:28]
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// لوحة العدادات
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 1) المسافة (تتغير دائماً)
|
||||
_buildLiveMetric(
|
||||
icon: Icons.alt_route_rounded,
|
||||
iconColor: Colors.blueAccent,
|
||||
value: controller.currentRideDistanceKm.toStringAsFixed(2),
|
||||
unit: 'KM'.tr,
|
||||
label: 'Traveled'.tr,
|
||||
),
|
||||
|
||||
_buildVerticalDivider(),
|
||||
|
||||
// 2) السعر (ثابت أو متغير)
|
||||
_buildLiveMetric(
|
||||
icon: isFixed
|
||||
? Icons.lock_outline_rounded
|
||||
: Icons.attach_money_rounded,
|
||||
iconColor: isFixed ? Colors.grey : AppColor.primaryColor,
|
||||
value: NumberFormat('#,##0').format(
|
||||
double.tryParse(controller.price.toString()) ?? 0,
|
||||
),
|
||||
unit: 'SYP'.tr,
|
||||
label: isFixed ? 'Fixed Price'.tr : 'Meter Fare'.tr,
|
||||
isHighlight: true,
|
||||
isFixedStyle: isFixed,
|
||||
),
|
||||
|
||||
_buildVerticalDivider(),
|
||||
|
||||
// 3) الوقت (تصغير الخط + إخفاء الساعات إذا 0)
|
||||
_buildLiveMetric(
|
||||
icon: Icons.timer_outlined,
|
||||
iconColor: Colors.orange,
|
||||
value: _formatDurationFromStart(controller),
|
||||
unit: '',
|
||||
label: 'Duration'.tr,
|
||||
valueFontSize: 14, // ✅ تصغير خط “المدة”
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// زر الإنهاء
|
||||
SlideAction(
|
||||
key: ValueKey(controller.isRideFinished),
|
||||
height: 60,
|
||||
borderRadius: 18,
|
||||
elevation: 0,
|
||||
text: 'Swipe to End Trip'.tr,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
outerColor: AppColor.redColor,
|
||||
innerColor: Colors.white,
|
||||
sliderButtonIcon: const Icon(
|
||||
Icons.stop_circle_outlined,
|
||||
color: AppColor.redColor,
|
||||
size: 32,
|
||||
),
|
||||
onSubmit: () async {
|
||||
await controller.finishRideFromDriver(isFromSlider: true);
|
||||
return null;
|
||||
},
|
||||
), // SlideAction مثال الاستخدام موجود في صفحة الحزمة [web:19]
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
); // GetBuilder يعيد البناء عند update() من الكنترولر [web:21]
|
||||
}
|
||||
|
||||
// Changed: تم تعديل مكان ومظهر دائرة السرعة
|
||||
// غيرنا نوع الإرجاع إلى Widget بدلاً من GetBuilder
|
||||
Widget speedCircle() {
|
||||
// التحقق من السرعة يمكن أن يبقى هنا أو داخل الـ builder
|
||||
// لكن التنبيهات (Vibration/Dialog) يفضل أن تكون داخل الـ builder لتجنب تكرارها أثناء إعادة البناء الخارجية
|
||||
/// دالة تنسيق المدة:
|
||||
/// - أقل من ساعة: mm:ss
|
||||
/// - ساعة فأكثر: h:mm:ss (خانة واحدة للساعات بدون leading zero)
|
||||
String _formatDurationFromStart(MapDriverController controller) {
|
||||
if (controller.rideStartTime == null) return "00:00";
|
||||
|
||||
return Positioned(
|
||||
// New: Positioned الآن هي الوالد المباشر (يجب وضع هذه الدالة داخل Stack في الصفحة الرئيسية)
|
||||
bottom: 25,
|
||||
left: 3,
|
||||
child: GetBuilder<MapDriverController>(
|
||||
builder: (controller) {
|
||||
// التحقق من التنبيهات هنا
|
||||
if (controller.speed > 100) {
|
||||
// نستخدم addPostFrameCallback لضمان عدم استدعاء الـ Dialog أثناء عملية البناء
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!Get.isDialogOpen!) {
|
||||
// تجنب فتح أكثر من نافذة
|
||||
if (Platform.isIOS) {
|
||||
HapticFeedback.selectionClick();
|
||||
} else {
|
||||
Vibration.vibrate(duration: 1000);
|
||||
}
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
titleStyle: AppStyle.title,
|
||||
title: 'Speed Over'.tr,
|
||||
middleText: 'Please slow down'.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'I will slow down'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
final d = DateTime.now().difference(controller.rideStartTime!);
|
||||
|
||||
return controller.isRideStarted
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
boxShadow: const [
|
||||
BoxShadow(blurRadius: 5, color: Colors.black26)
|
||||
],
|
||||
border: Border.all(
|
||||
width: 4,
|
||||
color: controller.speed > 100
|
||||
? Colors.red
|
||||
: AppColor.greenColor,
|
||||
),
|
||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||
|
||||
final hours = d.inHours;
|
||||
final minutes = d.inMinutes.remainder(60);
|
||||
final seconds = d.inSeconds.remainder(60);
|
||||
|
||||
if (hours == 0) {
|
||||
// mm:ss
|
||||
final totalMinutes = d.inMinutes;
|
||||
return "${twoDigits(totalMinutes)}:${twoDigits(seconds)}";
|
||||
}
|
||||
|
||||
// h:mm:ss
|
||||
return "$hours:${twoDigits(minutes)}:${twoDigits(seconds)}";
|
||||
} // Duration وتفكيكه (inHours/inMinutes/inSeconds) من أساسيات Dart [web:11]
|
||||
|
||||
Widget _buildLiveMetric({
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required String value,
|
||||
required String unit,
|
||||
required String label,
|
||||
bool isHighlight = false,
|
||||
bool isFixedStyle = false,
|
||||
double? valueFontSize, // ✅ جديد: حجم خط القيمة فقط
|
||||
}) {
|
||||
final effectiveFontSize = valueFontSize ?? (isHighlight ? 20 : 18);
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: iconColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: effectiveFontSize, // ✅ هنا صار التحكم
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isFixedStyle
|
||||
? Colors.grey[800]
|
||||
: (isHighlight ? AppColor.primaryColor : Colors.black87),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (unit.isNotEmpty) ...[
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
unit,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
height: 70,
|
||||
width: 70,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
controller.speed.toStringAsFixed(0),
|
||||
style: AppStyle.number.copyWith(fontSize: 24),
|
||||
),
|
||||
const Text("km/h", style: TextStyle(fontSize: 10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(); // إذا لم تبدأ الرحلة نخفي العنصر وهو داخل الـ Positioned
|
||||
},
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalDivider() {
|
||||
return Container(height: 35, width: 1, color: Colors.grey.shade300);
|
||||
}
|
||||
|
||||
@@ -55,16 +55,16 @@ class GoogleDriverMap extends StatelessWidget {
|
||||
trafficEnabled: true, // Changed: تفعيل عرض حركة المرور
|
||||
buildingsEnabled: true,
|
||||
polylines: {
|
||||
Polyline(
|
||||
zIndex: 2,
|
||||
// Polyline(
|
||||
// zIndex: 2,
|
||||
|
||||
polylineId: const PolylineId('route1'),
|
||||
points: controller.polylineCoordinates,
|
||||
color: const Color.fromARGB(255, 163, 81, 246),
|
||||
width: 6, // Changed: زيادة عرض الخط
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
),
|
||||
// polylineId: const PolylineId('route1'),
|
||||
// points: controller.polylineCoordinates,
|
||||
// color: const Color.fromARGB(255, 163, 81, 246),
|
||||
// width: 6, // Changed: زيادة عرض الخط
|
||||
// startCap: Cap.roundCap,
|
||||
// endCap: Cap.roundCap,
|
||||
// ),
|
||||
// Polyline(
|
||||
// zIndex: 2,
|
||||
|
||||
|
||||
@@ -12,43 +12,51 @@ class GoogleMapApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (mapDriverController) => mapDriverController.isRideStarted
|
||||
? Positioned(
|
||||
right: 3,
|
||||
bottom: 20,
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
var startLat = Get.find<MapDriverController>()
|
||||
.latLngPassengerLocation
|
||||
.latitude;
|
||||
var startLng = Get.find<MapDriverController>()
|
||||
.latLngPassengerLocation
|
||||
.longitude;
|
||||
builder: (mapDriverController) {
|
||||
if (!mapDriverController.isRideStarted) return const SizedBox();
|
||||
|
||||
var endLat = Get.find<MapDriverController>()
|
||||
.latLngPassengerDestination
|
||||
.latitude;
|
||||
var endLng = Get.find<MapDriverController>()
|
||||
.latLngPassengerDestination
|
||||
.longitude;
|
||||
// REMOVED: Positioned wrapper
|
||||
return Material(
|
||||
elevation: 8,
|
||||
shadowColor: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
onTap: () async {
|
||||
var endLat =
|
||||
mapDriverController.latLngPassengerDestination.latitude;
|
||||
var endLng =
|
||||
mapDriverController.latLngPassengerDestination.longitude;
|
||||
String url = 'google.navigation:q=$endLat,$endLng';
|
||||
|
||||
String url =
|
||||
'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/&directionsmode=driving';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} else {
|
||||
throw 'Could not launch google maps';
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
MaterialCommunityIcons.map_marker_radius,
|
||||
size: 45,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
)),
|
||||
)
|
||||
: const SizedBox());
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} else {
|
||||
String webUrl =
|
||||
'https://www.google.com/maps/dir/?api=1&destination=$endLat,$endLng';
|
||||
if (await canLaunchUrl(Uri.parse(webUrl))) {
|
||||
await launchUrl(Uri.parse(webUrl));
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueAccent,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(
|
||||
color: AppColor.blueColor.withOpacity(0.2), width: 1),
|
||||
),
|
||||
child: const Icon(
|
||||
MaterialCommunityIcons.google_maps,
|
||||
size: 28,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,135 +2,335 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../main.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
import '../../../../controller/functions/launch.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../widgets/error_snakbar.dart';
|
||||
|
||||
class PassengerInfoWindow extends StatelessWidget {
|
||||
PassengerInfoWindow({super.key});
|
||||
|
||||
// Optimization: defining static styles here avoids rebuilding them every frame
|
||||
final TextStyle _labelStyle =
|
||||
AppStyle.title.copyWith(color: Colors.grey[600], fontSize: 13);
|
||||
final TextStyle _valueStyle =
|
||||
AppStyle.title.copyWith(fontWeight: FontWeight.bold, fontSize: 18);
|
||||
const PassengerInfoWindow({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get safe area top padding (for Notches/Status bars)
|
||||
final double topPadding = MediaQuery.of(context).padding.top;
|
||||
final double topMargin = topPadding + 10; // Safe area + 10px spacing
|
||||
// 1. حساب الهوامش الآمنة (SafeArea) من الأسفل
|
||||
final double safeBottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (controller) => AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOut,
|
||||
// FIX: Use calculated top margin to avoid hiding behind status bar
|
||||
top: controller.isPassengerInfoWindow ? topMargin : -250.0,
|
||||
left: 15.0,
|
||||
right: 15.0,
|
||||
child: Card(
|
||||
// Optimization: Lower elevation slightly for smoother animation on cheap phones
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.2),
|
||||
color: Colors.white,
|
||||
surfaceTintColor: Colors.white, // Fix for Material 3 tinting
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
// id: 'PassengerInfo',
|
||||
builder: (controller) {
|
||||
// --- 1. تجهيز بيانات العرض ---
|
||||
String displayName = controller.passengerName ?? "Unknown";
|
||||
String avatarText = "";
|
||||
|
||||
// التحقق من اللغة (عربي/إنجليزي) للاسم المختصر
|
||||
bool isArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(displayName);
|
||||
|
||||
if (displayName.isNotEmpty) {
|
||||
if (isArabic) {
|
||||
avatarText = displayName.split(' ').first;
|
||||
if (avatarText.length > 4) {
|
||||
avatarText = avatarText.substring(0, 4);
|
||||
}
|
||||
} else {
|
||||
avatarText = displayName[0].toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. المنطق الذكي للموقع (Smart Positioning) ---
|
||||
// نرفع النافذة إذا ظهر شريط التعليمات في الأسفل لتجنب التغطية
|
||||
bool hasInstructions = controller.currentInstruction.isNotEmpty;
|
||||
double instructionsHeight = hasInstructions ? 110.0 : 0.0;
|
||||
|
||||
// الموقع النهائي: إذا كانت مفعلة تظهر، وإلا تختفي للأسفل
|
||||
double finalBottomPosition = controller.isPassengerInfoWindow
|
||||
? (safeBottomPadding + 10 + instructionsHeight)
|
||||
: -450.0;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
bottom: finalBottomPosition,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildTopInfoRow(controller),
|
||||
const Divider(height: 16),
|
||||
// --- مقبض السحب (Visual Handle) ---
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 8, bottom: 4),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (!controller.isRideBegin) _buildActionButtons(controller),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- الصف العلوي: معلومات الراكب ---
|
||||
Row(
|
||||
children: [
|
||||
// الصورة الرمزية
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color:
|
||||
AppColor.primaryColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
avatarText,
|
||||
style: TextStyle(
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: isArabic ? 14 : 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Optimization: Only render linear indicator if needed
|
||||
if (controller.remainingTimeInPassengerLocatioWait < 300 &&
|
||||
controller.remainingTimeInPassengerLocatioWait != 0 &&
|
||||
!controller.isRideBegin) ...[
|
||||
const SizedBox(height: 10),
|
||||
_buildWaitingIndicator(controller),
|
||||
],
|
||||
// النصوص (الاسم والمسافة)
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.location_on,
|
||||
size: 14, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${controller.distance} km',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Icon(Icons.access_time_filled,
|
||||
size: 14, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.hours > 0
|
||||
? '${controller.hours}h ${controller.minutes}m'
|
||||
: '${controller.minutes} min',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (controller.isdriverWaitTimeEnd &&
|
||||
!controller.isRideBegin) ...[
|
||||
const SizedBox(height: 10),
|
||||
_buildCancelAfterWaitButton(controller),
|
||||
]
|
||||
// أزرار جانبية (سرعة + اتصال)
|
||||
Row(
|
||||
children: [
|
||||
_buildSpeedCircle(),
|
||||
const SizedBox(width: 10),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
controller.isSocialPressed = true;
|
||||
|
||||
// نفحص النتيجة: هل مسموح له يتصل؟
|
||||
bool canCall =
|
||||
await controller.driverCallPassenger();
|
||||
|
||||
if (canCall) {
|
||||
makePhoneCall(
|
||||
controller.passengerPhone.toString());
|
||||
} else {
|
||||
// هنا ممكن تظهر رسالة: تم منع الاتصال بسبب كثرة الإلغاءات
|
||||
mySnackeBarError(
|
||||
"You cannot call the passenger due to policy violations"
|
||||
.tr);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.green.withOpacity(0.2)),
|
||||
),
|
||||
child: const Icon(Icons.phone,
|
||||
color: Colors.green, size: 22),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// خط فاصل
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Divider(height: 1, color: Colors.grey.shade100),
|
||||
),
|
||||
|
||||
// --- مؤشر الانتظار (يظهر عند الوصول) ---
|
||||
if (controller.remainingTimeInPassengerLocatioWait <
|
||||
300 &&
|
||||
controller.remainingTimeInPassengerLocatioWait != 0 &&
|
||||
!controller.isRideBegin) ...[
|
||||
_buildWaitingIndicator(controller),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// --- الأزرار الرئيسية (وصلت / ابدأ) ---
|
||||
if (!controller.isRideBegin)
|
||||
_buildActionButtons(controller),
|
||||
|
||||
// --- زر الإلغاء المدفوع (بعد انتهاء وقت الانتظار) ---
|
||||
if (controller.isdriverWaitTimeEnd &&
|
||||
!controller.isRideBegin)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFFF0F0),
|
||||
foregroundColor: Colors.red,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(
|
||||
color: Color(0xFFFFCDCD)),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
MyDialog().getDialog(
|
||||
'Confirm Cancellation'.tr,
|
||||
'Are you sure you want to cancel and collect the fee?'
|
||||
.tr, () async {
|
||||
// كود الإلغاء
|
||||
Get.back();
|
||||
controller
|
||||
.addWaitingTimeCostFromPassengerToDriverWallet();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.money_off, size: 20),
|
||||
label: Text('Cancel & Collect Fee'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopInfoRow(MapDriverController controller) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start, // Align top
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Go to passenger:'.tr, style: _labelStyle),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
controller.passengerName ?? 'loading...',
|
||||
style: _valueStyle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
// --- Widgets مساعدة ---
|
||||
|
||||
Widget _buildSpeedCircle() {
|
||||
return GetBuilder<LocationController>(builder: (locController) {
|
||||
int speedKmh = (locController.speed * 3.6).round();
|
||||
Color color = speedKmh > 100 ? Colors.red : const Color(0xFF0D47A1);
|
||||
|
||||
return Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 2),
|
||||
),
|
||||
const SizedBox(width: 10), // Spacing between name and chips
|
||||
Column(
|
||||
// Changed to Column for better layout on small screens
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildInfoChip(Icons.map_outlined, '${controller.distance} km'),
|
||||
const SizedBox(height: 6), // Vertical spacing
|
||||
_buildInfoChip(
|
||||
Icons.timer_outlined,
|
||||
controller.hours > 1
|
||||
? '${controller.hours}h ${controller.minutes}m'
|
||||
: '${controller.minutes}m',
|
||||
),
|
||||
Text('$speedKmh',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 13,
|
||||
height: 1)),
|
||||
Text('km/h',
|
||||
style: TextStyle(
|
||||
color: color.withOpacity(0.7), fontSize: 8, height: 1)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildInfoChip(IconData icon, String text) {
|
||||
Widget _buildWaitingIndicator(MapDriverController controller) {
|
||||
bool isUrgent = controller.remainingTimeInPassengerLocatioWait < 60;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: AppColor.primaryColor, size: 14), // Smaller icon
|
||||
const SizedBox(width: 6),
|
||||
Icon(Icons.timer_outlined,
|
||||
size: 16, color: isUrgent ? Colors.red : Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
controller.progressInPassengerLocationFromDriver.toDouble(),
|
||||
backgroundColor: Colors.grey[200],
|
||||
color: isUrgent ? Colors.red : Colors.green,
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
text,
|
||||
controller.stringRemainingTimeWaitingPassenger,
|
||||
style: TextStyle(
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12 // Slightly smaller font for chips
|
||||
),
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isUrgent ? Colors.red : Colors.green,
|
||||
fontFamily: 'monospace'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -138,155 +338,55 @@ class PassengerInfoWindow extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(MapDriverController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
if (controller.isArrivedSend)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: SizedBox(
|
||||
height: 45, // Fixed height for consistency
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.yellowColor,
|
||||
foregroundColor: Colors.black,
|
||||
padding: EdgeInsets.zero, // Reduce padding to fit text
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () async {
|
||||
// LOGIC FIX: Check distance FIRST
|
||||
double distance = await controller
|
||||
.calculateDistanceBetweenDriverAndPassengerLocation();
|
||||
|
||||
if (distance < 140) {
|
||||
// Only draw route and send notif if close enough
|
||||
controller.getRoute(
|
||||
origin: controller.latLngPassengerLocation,
|
||||
destination: controller.latLngPassengerDestination,
|
||||
routeColor: Colors.blue);
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'Hi ,I Arrive your site'.tr,
|
||||
body: 'I Arrive at your site'.tr,
|
||||
isTopic: false,
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
category: 'Hi ,I Arrive your site',
|
||||
);
|
||||
controller.startTimerToShowDriverWaitPassengerDuration();
|
||||
controller.isArrivedSend = false;
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'You are not near'.tr, // Shortened title
|
||||
'Please go to the pickup location exactly'.tr,
|
||||
() => Get.back());
|
||||
}
|
||||
},
|
||||
// Using Row instead of .icon constructor for better control
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text('I Arrive'.tr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.isArrivedSend) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFF1C40F),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
if (controller.isArrivedSend) const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 2, // Give "Start" button more space
|
||||
child: SizedBox(
|
||||
height: 45,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () {
|
||||
MyDialog().getDialog(
|
||||
"Is the Passenger in your Car?".tr,
|
||||
"Don't start trip if passenger not in your car".tr,
|
||||
() async {
|
||||
await controller.startRideFromDriver();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
onPressed: () async {
|
||||
await controller.markDriverAsArrived();
|
||||
},
|
||||
icon: const Icon(Icons.near_me_rounded),
|
||||
label: Text('I Have Arrived'.tr,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF27AE60),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: () {
|
||||
MyDialog().getDialog(
|
||||
"Start Trip?".tr,
|
||||
"Ensure the passenger is in the car.".tr,
|
||||
() async {
|
||||
await controller.startRideFromDriver();
|
||||
Get.back();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.play_arrow_rounded, size: 22),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text('Start the Ride'.tr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.play_circle_fill_rounded),
|
||||
label: Text('Start Ride'.tr,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaitingIndicator(MapDriverController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: AppColor.greyColor.withOpacity(0.2),
|
||||
// Ternary for color is fine
|
||||
color: controller.remainingTimeInPassengerLocatioWait < 60
|
||||
? AppColor.redColor
|
||||
: AppColor.greenColor,
|
||||
minHeight: 8, // Thinner looks more modern
|
||||
value: controller.progressInPassengerLocationFromDriver.toDouble(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"${'Waiting'.tr}: ${controller.stringRemainingTimeWaitingPassenger}",
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCancelAfterWaitButton(MapDriverController controller) {
|
||||
return MyElevatedButton(
|
||||
title: 'Cancel Trip & Get Cost'.tr, // Shortened text
|
||||
kolor: AppColor.gold,
|
||||
onPressed: () {
|
||||
MyDialog().getDialog('Are you sure to cancel?'.tr, '', () async {
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'Driver Cancelled Your Trip'.tr,
|
||||
body: 'You will need to pay the cost...',
|
||||
isTopic: false,
|
||||
tone: 'cancel',
|
||||
driverList: [],
|
||||
category: 'Driver Cancelled Your Trip',
|
||||
);
|
||||
box.write(BoxName.rideStatus, 'Cancel');
|
||||
await controller.addWaitingTimeCostFromPassengerToDriverWallet();
|
||||
controller.isdriverWaitTimeEnd = false;
|
||||
Get.back();
|
||||
});
|
||||
},
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,261 +1,89 @@
|
||||
// import 'dart:io';
|
||||
|
||||
// import 'package:bubble_head/bubble.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:sefer_driver/constant/info.dart';
|
||||
// import 'package:sefer_driver/controller/functions/location_controller.dart';
|
||||
// import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
// import 'package:sefer_driver/views/widgets/my_textField.dart';
|
||||
// import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
// import '../../../../constant/box_name.dart';
|
||||
// import '../../../../constant/colors.dart';
|
||||
// import '../../../../constant/style.dart';
|
||||
// import '../../../../controller/functions/launch.dart';
|
||||
// import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
// import '../../../../main.dart';
|
||||
|
||||
// class SosConnect extends StatelessWidget {
|
||||
// const SosConnect({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return GetBuilder<MapDriverController>(
|
||||
// builder: (mapDriverController) => mapDriverController.isRideStarted
|
||||
// ? Positioned(
|
||||
// left: 16,
|
||||
// bottom: 16,
|
||||
// child: Card(
|
||||
// elevation: 4,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// child: SizedBox(
|
||||
// height: 60,
|
||||
// width: 180,
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// IconButton(
|
||||
// onPressed: () {
|
||||
// _handleSosCall(mapDriverController);
|
||||
// },
|
||||
// icon: const Icon(
|
||||
// Icons.sos_sharp,
|
||||
// size: 32,
|
||||
// color: AppColor.redColor,
|
||||
// ),
|
||||
// tooltip: 'SOS - Call Emergency',
|
||||
// ),
|
||||
// VerticalDivider(
|
||||
// color: Colors.grey[300],
|
||||
// thickness: 1,
|
||||
// ),
|
||||
// IconButton(
|
||||
// onPressed: () {
|
||||
// _handleWhatsApp(mapDriverController);
|
||||
// },
|
||||
// icon: const Icon(
|
||||
// FontAwesome.whatsapp,
|
||||
// color: AppColor.greenColor,
|
||||
// size: 32,
|
||||
// ),
|
||||
// tooltip: 'SOS - Send WhatsApp Message',
|
||||
// ),
|
||||
// VerticalDivider(
|
||||
// color: Colors.grey[300],
|
||||
// thickness: 1,
|
||||
// ),
|
||||
// IconButton(
|
||||
// onPressed: () {
|
||||
// _handleGoogleMap(mapDriverController);
|
||||
// },
|
||||
// icon: const Icon(
|
||||
// MaterialCommunityIcons.map_marker_radius,
|
||||
// color: AppColor.primaryColor,
|
||||
// size: 32,
|
||||
// ),
|
||||
// tooltip: 'Google Maps - Navigate',
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// : const SizedBox(),
|
||||
// );
|
||||
// }
|
||||
|
||||
// void _handleSosCall(MapDriverController mapDriverController) {
|
||||
// if (box.read(BoxName.sosPhoneDriver) == null) {
|
||||
// Get.defaultDialog(
|
||||
// title: 'Insert Emergency Number'.tr,
|
||||
// content: Form(
|
||||
// key: mapDriverController.formKey1,
|
||||
// child: MyTextForm(
|
||||
// controller: mapDriverController.sosEmergincyNumberCotroller,
|
||||
// label: 'Emergency Number'.tr,
|
||||
// hint: 'Enter phone number'.tr,
|
||||
// type: TextInputType.phone,
|
||||
// ),
|
||||
// ),
|
||||
// confirm: MyElevatedButton(
|
||||
// title: 'Save'.tr,
|
||||
// onPressed: () {
|
||||
// if (mapDriverController.formKey1.currentState!.validate()) {
|
||||
// box.write(BoxName.sosPhoneDriver,
|
||||
// mapDriverController.sosEmergincyNumberCotroller.text);
|
||||
// Get.back(); // Close the dialog
|
||||
// launchCommunication(
|
||||
// 'phone', box.read(BoxName.sosPhoneDriver), '');
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// launchCommunication('phone', box.read(BoxName.sosPhoneDriver), '');
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _handleWhatsApp(MapDriverController mapDriverController) {
|
||||
// if (box.read(BoxName.sosPhoneDriver) == null) {
|
||||
// Get.defaultDialog(
|
||||
// title: 'Insert Emergency Number'.tr,
|
||||
// content: Form(
|
||||
// key: mapDriverController.formKey1,
|
||||
// child: MyTextForm(
|
||||
// controller: mapDriverController.sosEmergincyNumberCotroller,
|
||||
// label: 'Emergency Number'.tr,
|
||||
// hint: 'Enter phone number'.tr,
|
||||
// type: TextInputType.phone,
|
||||
// ),
|
||||
// ),
|
||||
// confirm: MyElevatedButton(
|
||||
// title: 'Save'.tr,
|
||||
// onPressed: () {
|
||||
// if (mapDriverController.formKey1.currentState!.validate()) {
|
||||
// box.write(BoxName.sosPhoneDriver,
|
||||
// mapDriverController.sosEmergincyNumberCotroller.text);
|
||||
// Get.back(); // Close the dialog
|
||||
// _sendWhatsAppMessage(mapDriverController);
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// _sendWhatsAppMessage(mapDriverController);
|
||||
// }
|
||||
// }
|
||||
|
||||
// void _handleGoogleMap(MapDriverController mapDriverController) {
|
||||
// () async {
|
||||
// if (Platform.isAndroid) {
|
||||
// Bubble().startBubbleHead(sendAppToBackground: true);
|
||||
// }
|
||||
// var startLat =
|
||||
// Get.find<MapDriverController>().latLngPassengerLocation.latitude;
|
||||
// var startLng =
|
||||
// Get.find<MapDriverController>().latLngPassengerLocation.longitude;
|
||||
|
||||
// var endLat =
|
||||
// Get.find<MapDriverController>().latLngPassengerDestination.latitude;
|
||||
// var endLng =
|
||||
// Get.find<MapDriverController>().latLngPassengerDestination.longitude;
|
||||
|
||||
// String url =
|
||||
// 'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/&directionsmode=driving';
|
||||
// if (await canLaunchUrl(Uri.parse(url))) {
|
||||
// await launchUrl(Uri.parse(url));
|
||||
// } else {
|
||||
// throw 'Could not launch google maps';
|
||||
// }
|
||||
// }();
|
||||
// }
|
||||
|
||||
// void _sendWhatsAppMessage(MapDriverController mapDriverController) {
|
||||
// final sosNumber = box.read(BoxName.sosPhoneDriver);
|
||||
// if (sosNumber != null) {
|
||||
// launchCommunication(
|
||||
// 'whatsapp',
|
||||
// '+2$sosNumber', // Consider international format
|
||||
// "${"Hello, this is Driver".tr} ${box.read(BoxName.nameDriver)}. "
|
||||
// "${"My current location is:".tr} "
|
||||
// "https://www.google.com/maps/place/"
|
||||
// "${Get.find<LocationController>().myLocation.latitude},"
|
||||
// "${Get.find<LocationController>().myLocation.longitude} "
|
||||
// "${"\nI have a trip on".tr} ${AppInformation.appName} "
|
||||
// "${"app with passenger".tr} ${mapDriverController.passengerName}.",
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_textField.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart'; // Checked import
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/firebase/firbase_messge.dart';
|
||||
import '../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../controller/functions/launch.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../../main.dart';
|
||||
|
||||
// Changed: إعادة تصميم وتغيير موضع أزرار التواصل والطوارئ
|
||||
class SosConnect extends StatelessWidget {
|
||||
SosConnect({super.key});
|
||||
final fcm = Get.isRegistered<FirebaseMessagesController>()
|
||||
? Get.find<FirebaseMessagesController>()
|
||||
: Get.put(FirebaseMessagesController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
id: 'SosConnect', // Keep ID for updates
|
||||
builder: (controller) {
|
||||
// New: تجميع الأزرار في عمود واحد على الجانب الأيمن
|
||||
return Positioned(
|
||||
bottom: 110, // New: فوق عداد السرعة
|
||||
right: 16,
|
||||
// Check visibility logic
|
||||
bool showPassengerContact =
|
||||
!controller.isRideBegin && controller.isPassengerInfoWindow;
|
||||
bool showSos = controller.isRideStarted;
|
||||
|
||||
if (!showPassengerContact && !showSos) return const SizedBox();
|
||||
|
||||
// REMOVED: Positioned widget
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// زر الاتصال بالراكب (يظهر قبل بدء الرحلة)
|
||||
if (!controller.isRideBegin && controller.isPassengerInfoWindow)
|
||||
_buildSocialButton(
|
||||
icon: Icons.phone,
|
||||
color: AppColor.blueColor,
|
||||
// === Call Button ===
|
||||
if (showPassengerContact)
|
||||
_buildModernActionButton(
|
||||
icon: Icons.phone_in_talk,
|
||||
color: Colors.white,
|
||||
bgColor: AppColor.blueColor,
|
||||
tooltip: 'Call Passenger',
|
||||
onPressed: () async {
|
||||
onTap: () async {
|
||||
controller.isSocialPressed = true;
|
||||
await controller.driverCallPassenger();
|
||||
makePhoneCall(controller.passengerPhone.toString());
|
||||
bool canCall = await controller.driverCallPassenger();
|
||||
if (canCall) {
|
||||
makePhoneCall(controller.passengerPhone.toString());
|
||||
} else {
|
||||
mySnackeBarError("Policy restriction on calls".tr);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// زر الرسائل للراكب (يظهر قبل بدء الرحلة)
|
||||
if (!controller.isRideBegin && controller.isPassengerInfoWindow)
|
||||
const SizedBox(height: 12),
|
||||
if (!controller.isRideBegin && controller.isPassengerInfoWindow)
|
||||
_buildSocialButton(
|
||||
icon: Icons.message,
|
||||
color: AppColor.greenColor,
|
||||
tooltip: 'Send Message',
|
||||
onPressed: () {
|
||||
// الكود الخاص بنافذة الرسائل السريعة
|
||||
_showMessageOptions(context, controller);
|
||||
},
|
||||
if (showPassengerContact) const SizedBox(height: 12),
|
||||
|
||||
// === Message Button ===
|
||||
if (showPassengerContact)
|
||||
_buildModernActionButton(
|
||||
icon: MaterialCommunityIcons.message_text_outline,
|
||||
color: AppColor.primaryColor,
|
||||
bgColor: Colors.grey.shade100,
|
||||
tooltip: 'Message Passenger',
|
||||
onTap: () => _showMessageOptions(context, controller),
|
||||
),
|
||||
|
||||
// زر الطوارئ (SOS) (يظهر بعد بدء الرحلة)
|
||||
if (controller.isRideStarted)
|
||||
_buildSocialButton(
|
||||
icon: Icons.sos_sharp,
|
||||
color: AppColor.redColor,
|
||||
tooltip: 'SOS - Call Emergency',
|
||||
onPressed: () => _handleSosCall(controller),
|
||||
// === SOS Button ===
|
||||
if (showSos)
|
||||
_buildModernActionButton(
|
||||
icon: MaterialIcons.warning,
|
||||
color: Colors.white,
|
||||
bgColor: AppColor.redColor,
|
||||
tooltip: 'EMERGENCY SOS',
|
||||
isPulsing: true,
|
||||
onTap: () => _handleSosCall(controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -264,42 +92,62 @@ class SosConnect extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// New: ودجت منفصل لبناء أزرار التواصل
|
||||
Widget _buildSocialButton(
|
||||
{required IconData icon,
|
||||
required Color color,
|
||||
required String tooltip,
|
||||
required VoidCallback onPressed}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black26)],
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, color: color, size: 28),
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
Widget _buildModernActionButton({
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
required String tooltip,
|
||||
required VoidCallback onTap,
|
||||
bool isPulsing = false,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: isPulsing
|
||||
? [
|
||||
BoxShadow(
|
||||
color: bgColor.withOpacity(0.4),
|
||||
blurRadius: 12,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// الكود الخاص بنافذة إدخال رقم الطوارئ
|
||||
// --- Logic Functions ---
|
||||
void _handleSosCall(MapDriverController mapDriverController) {
|
||||
if (box.read(BoxName.sosPhoneDriver) == null) {
|
||||
Get.defaultDialog(
|
||||
title: 'Insert Emergency Number'.tr,
|
||||
content: Form(
|
||||
key: mapDriverController.formKey1,
|
||||
child: MyTextForm(
|
||||
controller: mapDriverController.sosEmergincyNumberCotroller,
|
||||
label: 'Emergency Number'.tr,
|
||||
hint: 'Enter phone number'.tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
title: 'Emergency Contact'.tr,
|
||||
content: Column(
|
||||
children: [
|
||||
Text('Please enter the emergency number.'.tr),
|
||||
Form(
|
||||
key: mapDriverController.formKey1,
|
||||
child: MyTextForm(
|
||||
controller: mapDriverController.sosEmergincyNumberCotroller,
|
||||
label: 'Phone Number'.tr,
|
||||
hint: '01xxxxxxxxx',
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
title: 'Save & Call'.tr,
|
||||
onPressed: () {
|
||||
if (mapDriverController.formKey1.currentState!.validate()) {
|
||||
box.write(BoxName.sosPhoneDriver,
|
||||
@@ -316,120 +164,70 @@ class SosConnect extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// New: الكود الخاص بنافذة الرسائل السريعة (مستخرج من passenger_info_window.dart)
|
||||
void _showMessageOptions(
|
||||
BuildContext context, MapDriverController controller) {
|
||||
Get.bottomSheet(
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildMessageOptions(controller),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageOptions(MapDriverController controller) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Select a quick message'.tr, style: AppStyle.title),
|
||||
const SizedBox(height: 16),
|
||||
_buildMessageTile(
|
||||
text: "Where are you, sir?".tr,
|
||||
onTap: () {
|
||||
// fcm.sendNotificationToDriverMAP(
|
||||
// 'message From Driver',
|
||||
// "Where are you, sir?".tr,
|
||||
// controller.tokenPassenger,
|
||||
// [],
|
||||
// 'ding');
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'message From Driver'.tr,
|
||||
body: "Where are you, sir?".tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [], category: 'message From Driver',
|
||||
);
|
||||
Get.back();
|
||||
}),
|
||||
_buildMessageTile(
|
||||
text: "I've been trying to reach you but your phone is off.".tr,
|
||||
onTap: () {
|
||||
// fcm.sendNotificationToDriverMAP(
|
||||
// 'message From Driver',
|
||||
// "I've been trying to reach you but your phone is off.".tr,
|
||||
// controller.tokenPassenger,
|
||||
// [],
|
||||
// 'ding');
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'message From Driver'.tr,
|
||||
body: "I've been trying to reach you but your phone is off.".tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [], category: 'message From Driver',
|
||||
);
|
||||
Get.back();
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: controller.formKey2,
|
||||
child: MyTextForm(
|
||||
controller: controller.messageToPassenger,
|
||||
label: 'Type something'.tr,
|
||||
hint: 'Type something'.tr,
|
||||
type: TextInputType.text,
|
||||
Text('Quick Messages'.tr,
|
||||
style:
|
||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 15),
|
||||
_buildQuickMessageItem("Where are you, sir?".tr, controller),
|
||||
_buildQuickMessageItem("I've arrived.".tr, controller),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller.messageToPassenger,
|
||||
decoration:
|
||||
InputDecoration(hintText: 'Type a message...'.tr),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// fcm.sendNotificationToDriverMAP(
|
||||
// 'message From Driver',
|
||||
// controller.messageToPassenger.text,
|
||||
// controller.tokenPassenger,
|
||||
// [],
|
||||
// 'ding');
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'message From Driver'.tr,
|
||||
body: 'change device'.tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'cancel',
|
||||
driverList: [], category: 'message From Driver',
|
||||
);
|
||||
controller.messageToPassenger.clear();
|
||||
Get.back();
|
||||
},
|
||||
icon: const Icon(Icons.send),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
_sendMessage(controller, controller.messageToPassenger.text,
|
||||
'cancel');
|
||||
controller.messageToPassenger.clear();
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageTile(
|
||||
{required String text, required VoidCallback onTap}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey[100],
|
||||
),
|
||||
child: Text(text, style: AppStyle.title),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickMessageItem(String text, MapDriverController controller) {
|
||||
return ListTile(
|
||||
title: Text(text),
|
||||
onTap: () {
|
||||
_sendMessage(controller, text, 'ding');
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(MapDriverController controller, String body, String tone) {
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'Driver Message'.tr,
|
||||
body: body,
|
||||
isTopic: false,
|
||||
tone: tone,
|
||||
driverList: [],
|
||||
category: 'message From Driver',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
76
lib/views/home/Captin/mapDriverWidgets/sped_circle.dart
Normal file
76
lib/views/home/Captin/mapDriverWidgets/sped_circle.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
|
||||
// ويدجت للعرض فقط (بدون منطق فتح نوافذ)
|
||||
class SpeedCircle extends StatelessWidget {
|
||||
const SpeedCircle({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapDriverController>(
|
||||
id: 'SpeedCircle', // نحدد ID للتحديث الخفيف
|
||||
builder: (controller) {
|
||||
// إذا السرعة 0 أو أقل، نخفي الدائرة
|
||||
if (controller.speed <= 0) return const SizedBox();
|
||||
|
||||
bool isSpeeding = controller.speed > 100;
|
||||
|
||||
return Positioned(
|
||||
left: 20,
|
||||
top: 100, // مكانها المناسب
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
border: Border.all(
|
||||
color: _getSpeedColor(controller.speed),
|
||||
width: 4,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
controller.speed.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontFamily: AppStyle.title.fontFamily,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1.0,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"km/h",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Color _getSpeedColor(double speed) {
|
||||
if (speed < 60) return AppColor.greenColor;
|
||||
if (speed < 100) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
142
lib/views/home/Captin/orderCaptin/marker_generator.dart
Normal file
142
lib/views/home/Captin/orderCaptin/marker_generator.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class MarkerGenerator {
|
||||
// دالة لرسم ماركر يحتوي على نص (للوقت والمسافة)
|
||||
static Future<BitmapDescriptor> createCustomMarkerBitmap({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
required IconData iconData,
|
||||
}) async {
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
|
||||
// إعدادات القياسات
|
||||
const double width = 220.0;
|
||||
const double height = 110.0;
|
||||
const double circleRadius = 25.0;
|
||||
|
||||
// 1. رسم المربع (Info Box)
|
||||
final Paint paint = Paint()..color = color;
|
||||
final RRect rRect = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(0, 0, width, 60),
|
||||
const Radius.circular(15),
|
||||
);
|
||||
|
||||
// ظل خفيف
|
||||
canvas.drawShadow(Path()..addRRect(rRect), Colors.black, 5.0, true);
|
||||
canvas.drawRRect(rRect, paint);
|
||||
|
||||
// 2. رسم مثلث صغير أسفل المربع (Arrow Tail)
|
||||
final Path path = Path();
|
||||
path.moveTo(width / 2 - 10, 60);
|
||||
path.lineTo(width / 2, 75);
|
||||
path.lineTo(width / 2 + 10, 60);
|
||||
path.close();
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// 3. رسم الدائرة (مكان الأيقونة)
|
||||
canvas.drawCircle(const Offset(width / 2, 85), circleRadius, paint);
|
||||
|
||||
// 4. رسم الأيقونة داخل الدائرة
|
||||
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
iconPainter.text = TextSpan(
|
||||
text: String.fromCharCode(iconData.codePoint),
|
||||
style: TextStyle(
|
||||
fontSize: 30.0,
|
||||
fontFamily: iconData.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
iconPainter.layout();
|
||||
iconPainter.paint(
|
||||
canvas,
|
||||
Offset((width - iconPainter.width) / 2, 85 - (iconPainter.height / 2)),
|
||||
);
|
||||
|
||||
// 5. رسم النصوص (العنوان والوصف) داخل المربع
|
||||
// العنوان (مثلاً: المدة)
|
||||
TextPainter titlePainter = TextPainter(
|
||||
textDirection: TextDirection.rtl,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
titlePainter.text = TextSpan(
|
||||
text: title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
titlePainter.layout(minWidth: width);
|
||||
titlePainter.paint(canvas, const Offset(0, 8));
|
||||
|
||||
// الوصف (مثلاً: المسافة)
|
||||
TextPainter subTitlePainter = TextPainter(
|
||||
textDirection: TextDirection.rtl,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
subTitlePainter.text = TextSpan(
|
||||
text: subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
color: Colors.white70,
|
||||
),
|
||||
);
|
||||
subTitlePainter.layout(minWidth: width);
|
||||
subTitlePainter.paint(canvas, const Offset(0, 32));
|
||||
|
||||
// تحويل الرسم إلى صورة
|
||||
final ui.Image image = await pictureRecorder.endRecording().toImage(
|
||||
width.toInt(),
|
||||
(height + 20).toInt(), // مساحة إضافية
|
||||
);
|
||||
final ByteData? data =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
// دالة خاصة لرسم ماركر السائق (دائرة وخلفها سهم)
|
||||
static Future<BitmapDescriptor> createDriverMarker() async {
|
||||
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||
final Canvas canvas = Canvas(pictureRecorder);
|
||||
const double size = 100.0;
|
||||
|
||||
final Paint paint = Paint()..color = const Color(0xFF2E7D32); // أخضر غامق
|
||||
final Paint borderPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 4.0;
|
||||
|
||||
// الدائرة
|
||||
canvas.drawCircle(const Offset(size / 2, size / 2), size / 2.5, paint);
|
||||
canvas.drawCircle(
|
||||
const Offset(size / 2, size / 2), size / 2.5, borderPaint);
|
||||
|
||||
// رسم السهم (Arrow Up)
|
||||
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
iconPainter.text = TextSpan(
|
||||
text: String.fromCharCode(Icons.navigation.codePoint), // سهم ملاحة
|
||||
style: TextStyle(
|
||||
fontSize: 40.0,
|
||||
fontFamily: Icons.navigation.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
iconPainter.layout();
|
||||
iconPainter.paint(
|
||||
canvas,
|
||||
Offset((size - iconPainter.width) / 2, (size - iconPainter.height) / 2),
|
||||
);
|
||||
|
||||
final ui.Image image = await pictureRecorder
|
||||
.endRecording()
|
||||
.toImage(size.toInt(), size.toInt());
|
||||
final ByteData? data =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:sefer_driver/constant/api_key.dart';
|
||||
import 'package:sefer_driver/models/overlay_service.dart';
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/links.dart';
|
||||
import '../../../../controller/firebase/firbase_messge.dart';
|
||||
@@ -294,6 +296,14 @@ class _OrderOverlayState extends State<OrderOverlay>
|
||||
'ding',
|
||||
'',
|
||||
);
|
||||
|
||||
// 3. الخطوة الأهم: فتح التطبيق وإغلاق النافذة
|
||||
try {
|
||||
// استدعاء الميثود التي تم تحديثها في الخطوة 1
|
||||
await OverlayMethodChannel.bringToForeground();
|
||||
} catch (e) {
|
||||
_log("Failed to bring app to foreground: $e");
|
||||
}
|
||||
await _closeOverlay();
|
||||
} else {
|
||||
_log("Failed to update order status on server: $res");
|
||||
@@ -309,6 +319,12 @@ class _OrderOverlayState extends State<OrderOverlay>
|
||||
_log(
|
||||
"A critical error occurred during server update: $e\nStackTrace: $s");
|
||||
if (mounted) setState(() => buttonsEnabled = true);
|
||||
|
||||
_log("Error in accept order: $e");
|
||||
await _closeOverlay();
|
||||
// حتى في حال الخطأ، نحاول فتح التطبيق ليرى السائق ما حدث
|
||||
await OverlayMethodChannel.bringToForeground();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -342,7 +358,7 @@ class _OrderOverlayState extends State<OrderOverlay>
|
||||
_log("Driver ID is null, cannot refuse order");
|
||||
return;
|
||||
}
|
||||
_crud.post(link: AppLink.addDriverOrder, payload: {
|
||||
CRUD().post(link: AppLink.addDriverOrder, payload: {
|
||||
'driver_id': driverId,
|
||||
'order_id': orderID,
|
||||
'status': 'Refused'
|
||||
@@ -492,6 +508,11 @@ class _OrderOverlayState extends State<OrderOverlay>
|
||||
|
||||
Widget _buildPrimaryInfo() {
|
||||
final order = orderData!;
|
||||
|
||||
// FIX: Parse the price to a number safely before formatting
|
||||
// This handles cases where order.price is a String like "173"
|
||||
final num priceValue = num.tryParse(order.price.toString()) ?? 0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
@@ -508,10 +529,8 @@ class _OrderOverlayState extends State<OrderOverlay>
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildHighlightInfo(
|
||||
// التعديل هنا 👇
|
||||
"${NumberFormat('#,##0').format(order.price)} ل.س",
|
||||
// أو يمكنك استخدام "SYP" بدلاً من "ل.س"
|
||||
|
||||
// FIX: Use the parsed priceValue here
|
||||
"${NumberFormat('#,##0').format(priceValue)} ل.س",
|
||||
"السعر".tr,
|
||||
Icons.monetization_on_rounded,
|
||||
AppColors.priceHighlight,
|
||||
@@ -522,7 +541,8 @@ class _OrderOverlayState extends State<OrderOverlay>
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildHighlightInfo(
|
||||
"${order.tripDistanceKm.toStringAsFixed(1)} كم",
|
||||
// Ensure tripDistanceKm is treated safely too
|
||||
"${(num.tryParse(order.tripDistanceKm.toString()) ?? 0).toStringAsFixed(1)} كم",
|
||||
"المسافة".tr,
|
||||
Icons.straighten_rounded,
|
||||
AppColors.accent,
|
||||
|
||||
@@ -1,450 +1,411 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/controller/firebase/firbase_messge.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'dart:math' as math;
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/links.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../controller/functions/crud.dart';
|
||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../../controller/functions/launch.dart';
|
||||
import '../../../../controller/home/captin/order_request_controller.dart';
|
||||
import '../../../../print.dart';
|
||||
import '../../../widgets/elevated_btn.dart';
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/controller/home/captin/order_request_controller.dart';
|
||||
|
||||
class OrderRequestPage extends StatefulWidget {
|
||||
class OrderRequestPage extends StatelessWidget {
|
||||
const OrderRequestPage({super.key});
|
||||
|
||||
@override
|
||||
State<OrderRequestPage> createState() => _OrderRequestPageState();
|
||||
}
|
||||
|
||||
class _OrderRequestPageState extends State<OrderRequestPage> {
|
||||
final OrderRequestController orderRequestController =
|
||||
Get.put(OrderRequestController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حقن الكنترولر
|
||||
final OrderRequestController controller = Get.put(OrderRequestController());
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Order Request'.tr),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: GetBuilder<OrderRequestController>(
|
||||
builder: (controller) {
|
||||
if (controller.myList == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: Get.height * 0.3,
|
||||
child: GoogleMap(
|
||||
mapType: MapType.normal,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(controller.latPassengerLocation,
|
||||
controller.lngPassengerLocation),
|
||||
zoom: 14.0,
|
||||
body: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: GetBuilder<OrderRequestController>(
|
||||
builder: (controller) {
|
||||
// 🔥 التعديل الأهم: التحقق من وجود أي بيانات (List أو Map)
|
||||
if (controller.myList == null && controller.myMapData == null) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator()); // شاشة تحميل بدلاً من فراغ
|
||||
}
|
||||
|
||||
// 🔥 استخدام دوال الكنترولر الآمنة لجلب البيانات بدلاً من الوصول المباشر
|
||||
// قمت بتحويل _safeGet إلى دالة عامة safeGet في الكنترولر (تأكد من جعلها public)
|
||||
// أو سأقوم بكتابة المنطق هنا مباشرة لضمان العمل:
|
||||
|
||||
String getValue(int index) {
|
||||
if (controller.myList != null &&
|
||||
index < controller.myList!.length) {
|
||||
return controller.myList![index].toString();
|
||||
}
|
||||
if (controller.myMapData != null &&
|
||||
controller.myMapData!.containsKey(index.toString())) {
|
||||
return controller.myMapData![index.toString()].toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
final String passengerName =
|
||||
getValue(8).isEmpty ? "عميل" : getValue(8);
|
||||
final String startAddr =
|
||||
getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29);
|
||||
final String endAddr =
|
||||
getValue(30).isEmpty ? "الوجهة" : getValue(30);
|
||||
final bool isVisa = (getValue(13) == 'true');
|
||||
|
||||
// منطق Speed = سعر ثابت
|
||||
final bool isSpeed =
|
||||
controller.tripType.toLowerCase().contains('speed');
|
||||
final String carTypeLabel =
|
||||
isSpeed ? "سعر ثابت" : controller.tripType;
|
||||
final Color carTypeColor =
|
||||
isSpeed ? Colors.red.shade700 : Colors.blue.shade700;
|
||||
final IconData carIcon =
|
||||
isSpeed ? Icons.local_offer : Icons.directions_car;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// 1. الخارطة
|
||||
Positioned.fill(
|
||||
bottom: 300,
|
||||
child: GoogleMap(
|
||||
mapType: MapType.normal,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(
|
||||
controller.latPassenger, controller.lngPassenger),
|
||||
zoom: 13.0,
|
||||
),
|
||||
markers: controller.markers,
|
||||
polylines: controller.polylines,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 80, bottom: 20, left: 20, right: 20),
|
||||
onMapCreated: (c) {
|
||||
controller.onMapCreated(c);
|
||||
controller.update();
|
||||
},
|
||||
),
|
||||
myLocationButtonEnabled: true,
|
||||
onMapCreated: controller.onMapCreated,
|
||||
myLocationEnabled: true,
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: const MarkerId('startLocation'),
|
||||
position: LatLng(controller.latPassengerLocation,
|
||||
controller.lngPassengerLocation),
|
||||
icon: controller.startIcon,
|
||||
),
|
||||
Marker(
|
||||
markerId: const MarkerId('destinationLocation'),
|
||||
position: LatLng(controller.latPassengerDestination,
|
||||
controller.lngPassengerDestination),
|
||||
icon: controller.endIcon,
|
||||
),
|
||||
},
|
||||
polylines: {
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
color: AppColor.primaryColor,
|
||||
width: 5,
|
||||
points: controller.pointsDirection,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Card(
|
||||
elevation: 4,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
controller.myList[13].toString() == 'true'
|
||||
? Icons.credit_card
|
||||
: Icons.money,
|
||||
color: controller.myList[13].toString() == 'true'
|
||||
? AppColor.deepPurpleAccent
|
||||
: AppColor.greenColor,
|
||||
),
|
||||
title: Text(
|
||||
'Payment Method'.tr,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
trailing: Text(
|
||||
controller.myList[13].toString() == 'true'
|
||||
? 'Visa'
|
||||
: 'Cash',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
// 2. كبسولة الوصول للراكب
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 8)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.near_me,
|
||||
color: Colors.amber, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"الوصول للراكب: ${controller.timeToPassenger}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Card(
|
||||
elevation: 4,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.account_circle,
|
||||
color: AppColor.secondaryColor),
|
||||
title: Text(
|
||||
controller.myList[8],
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
const Icon(Icons.star,
|
||||
size: 16, color: Colors.amber),
|
||||
Text(
|
||||
controller.myList[33].toString(),
|
||||
style: const TextStyle(color: Colors.amber),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 3. البطاقة السفلية
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 360,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(25),
|
||||
topRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Card(
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on,
|
||||
color: AppColor.greenColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
// Keep Expanded here for layout
|
||||
child: Text(
|
||||
controller.myList[29],
|
||||
style:
|
||||
Theme.of(context).textTheme.titleSmall,
|
||||
maxLines: 2, // Allow up to 2 lines
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Handle overflow
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.flag,
|
||||
color: AppColor.redColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
// Keep Expanded here for layout
|
||||
child: Text(
|
||||
controller.myList[30],
|
||||
style:
|
||||
Theme.of(context).textTheme.titleSmall,
|
||||
maxLines: 2, // Allow up to 2 lines
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Handle overflow
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Card(
|
||||
// elevation: 4,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// _InfoTile(
|
||||
// icon: Icons.timer,
|
||||
// label:
|
||||
// '${(double.parse(controller.myList[12]) / 60).toStringAsFixed(0)} ${'min'.tr}',
|
||||
// ),
|
||||
// _InfoTile(
|
||||
// icon: Icons.directions_car,
|
||||
// label:
|
||||
// '${(double.parse(controller.myList[11]) / 1000).toStringAsFixed(1)} ${'km'.tr}',
|
||||
// ),
|
||||
// _InfoTile(
|
||||
// icon: Icons.monetization_on,
|
||||
// label: '${controller.myList[2]}',
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// استبدل هذا الكود
|
||||
Card(
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_InfoTile(
|
||||
icon: Icons.timer,
|
||||
label:
|
||||
// استخدم الفهرس 13 للوقت (Duration)
|
||||
'${((double.tryParse(controller.myList[13].toString()) ?? 0.0) / 60).toStringAsFixed(0)} ${'min'.tr}',
|
||||
),
|
||||
_InfoTile(
|
||||
icon: Icons.directions_car,
|
||||
label:
|
||||
// استخدم الفهرس 14 للمسافة (Distance)
|
||||
// استخدم tryParse للأمان لأن القيمة "" (نص فارغ)
|
||||
'${((double.tryParse(controller.myList[14].toString()) ?? 0.0) / 1000).toStringAsFixed(1)} ${'km'.tr}',
|
||||
),
|
||||
_InfoTile(
|
||||
icon: Icons.monetization_on,
|
||||
label:
|
||||
// السعر أصبح في الفهرس 4
|
||||
'${controller.myList[4]}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Accept Order'.tr,
|
||||
onPressed: () async {
|
||||
var res = await CRUD().post(
|
||||
link:
|
||||
"${AppLink.ride}/rides/updateStausFromSpeed.php",
|
||||
payload: {
|
||||
'id': (controller.myList[16]),
|
||||
'rideTimeStart': DateTime.now().toString(),
|
||||
'status': 'Apply',
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
});
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2)))),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
if (res == 'failure') {
|
||||
MyDialog().getDialog(
|
||||
"This ride is already applied by another driver."
|
||||
.tr,
|
||||
'', () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
});
|
||||
} else {
|
||||
Get.put(HomeCaptainController()).changeRideId();
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
controller.endTimer();
|
||||
controller.changeApplied();
|
||||
|
||||
CRUD().postFromDialogue(
|
||||
link: AppLink.addDriverOrder,
|
||||
payload: {
|
||||
'driver_id':
|
||||
(controller.myList[6].toString()),
|
||||
'order_id':
|
||||
(controller.myList[16].toString()),
|
||||
'status': 'Apply'
|
||||
});
|
||||
|
||||
List<String> bodyToPassenger = [
|
||||
controller.myList[6].toString(),
|
||||
controller.myList[8].toString(),
|
||||
controller.myList[9].toString(),
|
||||
];
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: controller.myList[9].toString(),
|
||||
title: "Accepted Ride".tr,
|
||||
body: 'your ride is Accepted'.tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'start',
|
||||
driverList: bodyToPassenger,
|
||||
category: 'Accepted Ride',
|
||||
);
|
||||
Get.back();
|
||||
box.write(BoxName.rideArguments, {
|
||||
'passengerLocation':
|
||||
controller.myList[0].toString(),
|
||||
'passengerDestination':
|
||||
controller.myList[1].toString(),
|
||||
'Duration': controller.myList[4].toString(),
|
||||
'totalCost': controller.myList[26].toString(),
|
||||
'Distance': controller.myList[5].toString(),
|
||||
'name': controller.myList[8].toString(),
|
||||
'phone': controller.myList[10].toString(),
|
||||
'email': controller.myList[28].toString(),
|
||||
'WalletChecked':
|
||||
controller.myList[13].toString(),
|
||||
'tokenPassenger':
|
||||
controller.myList[9].toString(),
|
||||
'direction':
|
||||
'https://www.google.com/maps/dir/${controller.myList[0]}/${controller.myList[1]}/',
|
||||
'DurationToPassenger':
|
||||
controller.myList[15].toString(),
|
||||
'rideId': (controller.myList[16].toString()),
|
||||
'passengerId':
|
||||
(controller.myList[7].toString()),
|
||||
'driverId': (controller.myList[18].toString()),
|
||||
'durationOfRideValue':
|
||||
controller.myList[19].toString(),
|
||||
'paymentAmount':
|
||||
controller.myList[2].toString(),
|
||||
'paymentMethod':
|
||||
controller.myList[13].toString() == 'true'
|
||||
? 'visa'
|
||||
: 'cash',
|
||||
'isHaveSteps': controller.myList[20].toString(),
|
||||
'step0': controller.myList[21].toString(),
|
||||
'step1': controller.myList[22].toString(),
|
||||
'step2': controller.myList[23].toString(),
|
||||
'step3': controller.myList[24].toString(),
|
||||
'step4': controller.myList[25].toString(),
|
||||
'passengerWalletBurc':
|
||||
controller.myList[26].toString(),
|
||||
'timeOfOrder': DateTime.now().toString(),
|
||||
'totalPassenger':
|
||||
controller.myList[2].toString(),
|
||||
'carType': controller.myList[31].toString(),
|
||||
'kazan': controller.myList[32].toString(),
|
||||
'startNameLocation':
|
||||
controller.myList[29].toString(),
|
||||
'endNameLocation':
|
||||
controller.myList[30].toString(),
|
||||
});
|
||||
Get.to(() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArguments));
|
||||
Log.print(
|
||||
'box.read(BoxName.rideArguments): ${box.read(BoxName.rideArguments)}');
|
||||
}
|
||||
},
|
||||
),
|
||||
GetBuilder<OrderRequestController>(
|
||||
builder: (timerController) {
|
||||
final isNearEnd = timerController.remainingTime <=
|
||||
5; // Define a threshold for "near end"
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
// الصف الأول: الراكب والسعر
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: timerController.progress,
|
||||
// Set the color based on the "isNearEnd" condition
|
||||
color: isNearEnd ? Colors.red : Colors.blue,
|
||||
const CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
child: Icon(Icons.person,
|
||||
color: Colors.grey, size: 28),
|
||||
),
|
||||
Text(
|
||||
'${timerController.remainingTime}',
|
||||
style: AppStyle.number,
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(passengerName,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star,
|
||||
color: Colors.amber, size: 14),
|
||||
Text(controller.passengerRating,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text("${controller.tripPrice} ل.س",
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primaryColor)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isVisa
|
||||
? Colors.purple.withOpacity(0.1)
|
||||
: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(isVisa ? "فيزا" : "كاش",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isVisa
|
||||
? Colors.purple
|
||||
: Colors.green)),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
isVisa
|
||||
? Icons.credit_card
|
||||
: Icons.money,
|
||||
size: 14,
|
||||
color: isVisa
|
||||
? Colors.purple
|
||||
: Colors.green),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
MyElevatedButton(
|
||||
title: 'Refuse Order'.tr,
|
||||
onPressed: () async {
|
||||
controller.endTimer();
|
||||
// List<String> bodyToPassenger = [
|
||||
// box.read(BoxName.driverID).toString(),
|
||||
// box.read(BoxName.nameDriver).toString(),
|
||||
// box.read(BoxName.tokenDriver).toString(),
|
||||
// ];
|
||||
|
||||
// NotificationService.sendNotification(
|
||||
// target: controller.myList[9].toString(),
|
||||
// title: 'Order Under Review'.tr,
|
||||
// body:
|
||||
// '${box.read(BoxName.nameDriver)} ${'is reviewing your order. They may need more information or a higher price.'.tr}',
|
||||
// isTopic: false, // Important: this is a token
|
||||
// tone: 'start',
|
||||
// driverList: bodyToPassenger,
|
||||
// category: 'Order Under Review',
|
||||
// );
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// controller.refuseOrder(
|
||||
// (controller.myList[16].toString()),
|
||||
// );
|
||||
controller.addRideToNotificationDriverString(
|
||||
controller.myList[16].toString(),
|
||||
controller.myList[29].toString(),
|
||||
controller.myList[30].toString(),
|
||||
'${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}',
|
||||
'${DateTime.now().hour}:${DateTime.now().minute}',
|
||||
controller.myList[2].toString(),
|
||||
controller.myList[7].toString(),
|
||||
'wait',
|
||||
controller.myList[31].toString(),
|
||||
controller.myList[33].toString(),
|
||||
controller.myList[2].toString(),
|
||||
controller.myList[5].toString(),
|
||||
controller.myList[4].toString());
|
||||
},
|
||||
kolor: AppColor.redColor,
|
||||
// الصف الثاني: شريط المعلومات
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
carIcon, carTypeLabel, carTypeColor),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Colors.grey.shade300),
|
||||
_buildInfoItem(Icons.route,
|
||||
controller.totalTripDistance, Colors.black87),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Colors.grey.shade300),
|
||||
_buildInfoItem(Icons.access_time_filled,
|
||||
controller.totalTripDuration, Colors.black87),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// الصف الثالث: العناوين
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const Icon(Icons.my_location,
|
||||
size: 18, color: Colors.green),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 2,
|
||||
color: Colors.grey.shade300,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 2))),
|
||||
const Icon(Icons.location_on,
|
||||
size: 18, color: Colors.red),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("موقع الانطلاق",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey)),
|
||||
Text(startAddr,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("الوجهة",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey)),
|
||||
Text(endAddr,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// الصف الرابع: الأزرار
|
||||
Row(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
shape: BoxShape.circle,
|
||||
border:
|
||||
Border.all(color: Colors.red.shade100)),
|
||||
child: const Icon(Icons.close,
|
||||
color: Colors.red, size: 24),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => controller.acceptOrder(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text("قبول الرحلة",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(width: 15),
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
value: controller.progress,
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
backgroundColor: Colors.white24),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text("${controller.remainingTime}",
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
const _InfoTile({required this.icon, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
Widget _buildInfoItem(IconData icon, String text, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: AppColor.primaryColor),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.bold, color: color)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -382,17 +382,13 @@ class WalletCaptainRefactored extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
PointsCaptain(
|
||||
kolor: AppColor.greyColor,
|
||||
pricePoint: 10000,
|
||||
countPoint: '10000'),
|
||||
kolor: AppColor.greyColor, pricePoint: 100, countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: AppColor.bronze, pricePoint: 20000, countPoint: '21000'),
|
||||
kolor: AppColor.bronze, pricePoint: 200, countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: AppColor.goldenBronze,
|
||||
pricePoint: 40000,
|
||||
countPoint: '45000'),
|
||||
kolor: AppColor.goldenBronze, pricePoint: 400, countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: AppColor.gold, pricePoint: 100000, countPoint: '110000'),
|
||||
kolor: AppColor.gold, pricePoint: 1000, countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/controller/profile/captain_profile_controller.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/criminal_documents_page.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import 'behavior_page.dart';
|
||||
import 'captains_cars.dart';
|
||||
|
||||
@@ -150,11 +155,13 @@ class ActionsGrid extends StatelessWidget {
|
||||
// onTap: () => Get.to(() => CriminalDocumemtPage()),
|
||||
// ),
|
||||
_ActionTile(
|
||||
title: 'Bank Account'.tr,
|
||||
icon: Icons.account_balance,
|
||||
title: 'ShamCash Account'.tr, // غيرت الاسم ليكون أوضح
|
||||
icon: Icons.account_balance_wallet_rounded, // أيقونة محفظة
|
||||
// trailing: Icon(Icons.arrow_forward_ios,
|
||||
// size: 16, color: Colors.grey), // سهم صغير للجمالية
|
||||
onTap: () {
|
||||
MyDialog().getDialog('Coming Soon'.tr,
|
||||
'This service will be available soon.'.tr, () => Get.back());
|
||||
// استدعاء دالة فتح النافذة
|
||||
showShamCashInput();
|
||||
},
|
||||
),
|
||||
_ActionTile(
|
||||
@@ -167,6 +174,223 @@ class ActionsGrid extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void showShamCashInput() {
|
||||
// 1. القراءة من الذاكرة المحلية (GetStorage) عند فتح النافذة
|
||||
// إذا لم يتم العثور على قيمة، يتم تعيينها إلى نص فارغ
|
||||
final String existingName = box.read('shamcash_name') ?? '';
|
||||
final String existingCode = box.read('shamcash_code') ?? '';
|
||||
|
||||
// تعريف أدوات التحكم للحقلين مع تحميل القيمة المحفوظة
|
||||
final TextEditingController nameController =
|
||||
TextEditingController(text: existingName);
|
||||
final TextEditingController codeController =
|
||||
TextEditingController(text: existingCode);
|
||||
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(25),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26, blurRadius: 10, offset: Offset(0, -2))
|
||||
],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// --- 1. المقبض العلوي ---
|
||||
Center(
|
||||
child: Container(
|
||||
height: 5,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
),
|
||||
),
|
||||
|
||||
// --- 2. العنوان والأيقونة ---
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
height: 50,
|
||||
),
|
||||
// const Icon(Icons.account_balance_wallet_rounded,
|
||||
// size: 45, color: Colors.blueAccent),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"ربط حساب شام كاش 🔗",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blueGrey[900]),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Text(
|
||||
"أدخل بيانات حسابك لاستقبال الأرباح فوراً",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// --- 3. الحقل الأول: اسم الحساب (أعلى الباركود) ---
|
||||
const Text("1. اسم الحساب (أعلى الباركود)",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "مثال: intaleq",
|
||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: const Icon(Icons.person_outline_rounded,
|
||||
color: Colors.blueGrey),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// --- 4. الحقل الثاني: الكود (أسفل الباركود) ---
|
||||
const Text("2. كود المحفظة (أسفل الباركود)",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: TextField(
|
||||
controller: codeController,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
letterSpacing: 0.5), // خط أصغر قليلاً للكود الطويل
|
||||
decoration: InputDecoration(
|
||||
hintText: "مثال: 80f23afe40...",
|
||||
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: const Icon(Icons.qr_code_2_rounded,
|
||||
color: Colors.blueGrey),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||
|
||||
// زر لصق الكود
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.paste_rounded, color: Colors.blue),
|
||||
tooltip: "لصق الكود",
|
||||
onPressed: () async {
|
||||
ClipboardData? data =
|
||||
await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
codeController.text = data.text!;
|
||||
// تحريك المؤشر للنهاية بعد اللصق
|
||||
codeController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: codeController.text.length),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// --- 5. زر الحفظ ---
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
String name = nameController.text.trim();
|
||||
String code = codeController.text.trim();
|
||||
|
||||
// التحقق من صحة البيانات
|
||||
if (name.isNotEmpty && code.length > 5) {
|
||||
// 1. إرسال البيانات إلى السيرفر
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.updateShamCashDriver, payload: {
|
||||
"id": box.read(BoxName.driverID),
|
||||
"accountBank": name,
|
||||
"bankCode": code,
|
||||
});
|
||||
|
||||
if (res != 'failure') {
|
||||
// 2. 🔴 الحفظ في الذاكرة المحلية (GetStorage) بعد نجاح التحديث
|
||||
box.write('shamcash_name', name);
|
||||
box.write('shamcash_code', code);
|
||||
|
||||
Get.back(); // إغلاق النافذة
|
||||
Get.snackbar(
|
||||
"تم الحفظ بنجاح",
|
||||
"تم ربط حساب ($name) لاستلام الأرباح.",
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(20),
|
||||
icon: const Icon(Icons.check_circle_outline,
|
||||
color: Colors.white),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
// في حال فشل الإرسال إلى السيرفر
|
||||
Get.snackbar(
|
||||
"خطأ في السيرفر",
|
||||
"فشل تحديث البيانات، يرجى المحاولة لاحقاً.",
|
||||
backgroundColor: Colors.redAccent,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(20),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"بيانات ناقصة",
|
||||
"يرجى التأكد من إدخال الاسم والكود بشكل صحيح.",
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
margin: const EdgeInsets.all(20),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2ecc71), // الأخضر المالي
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 2,
|
||||
),
|
||||
child: const Text(
|
||||
"حفظ وتفعيل الحساب",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10), // مسافة سفلية إضافية للأمان
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
isScrollControlled: true, // ضروري لرفع النافذة عند فتح الكيبورد
|
||||
);
|
||||
}
|
||||
|
||||
/// ويدجت داخلية لزر في الشبكة
|
||||
class _ActionTile extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
@@ -1,97 +1,108 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:math';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart'; // لتحديد الأنواع إذا لزم
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/firebase/firbase_messge.dart';
|
||||
import '../../controller/firebase/notification_service.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../../controller/home/captin/home_captain_controller.dart';
|
||||
import '../../controller/notification/ride_available_controller.dart';
|
||||
import '../../main.dart';
|
||||
import '../../main.dart'; // للوصول للـ box
|
||||
import '../home/Captin/driver_map_page.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import '../widgets/mycircular.dart';
|
||||
import '../widgets/mydialoug.dart';
|
||||
|
||||
// --- Placeholder Classes and Variables (for demonstration) ---
|
||||
// These are dummy implementations to make the code runnable.
|
||||
// You should use your actual project files.
|
||||
|
||||
// --- End of Placeholder Classes ---
|
||||
|
||||
class AvailableRidesPage extends StatelessWidget {
|
||||
const AvailableRidesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use findOrPut to avoid re-creating the controller on rebuilds
|
||||
// حقن الكنترولر (تأكد أنك تستخدم الكود الجديد الذي أعطيتك إياه للكنترولر)
|
||||
Get.lazyPut(() => RideAvailableController());
|
||||
Get.lazyPut(() => HomeCaptainController());
|
||||
|
||||
return GetBuilder<RideAvailableController>(
|
||||
builder: (rideAvailableController) {
|
||||
// rideAvailableController.sortRidesByDistance(); // Original logic
|
||||
return MyScafolld(
|
||||
builder: (controller) {
|
||||
return MyScafolld(
|
||||
title: 'Available for rides'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
rideAvailableController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
controller.isLoading
|
||||
? const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 50.0),
|
||||
child: MyCircularProgressIndicator(),
|
||||
))
|
||||
: Builder(
|
||||
builder: (context) {
|
||||
// Filtering logic remains the same
|
||||
final filteredRides = rideAvailableController
|
||||
.rideAvailableMap['message']
|
||||
.where((rideInfo) {
|
||||
var driverType =
|
||||
box.read(BoxName.carTypeOfDriver).toString();
|
||||
switch (driverType) {
|
||||
case 'Comfort':
|
||||
return ['Speed', 'Comfort']
|
||||
.contains(rideInfo['carType']);
|
||||
case 'Speed':
|
||||
case 'Scooter':
|
||||
case 'Awfar Car':
|
||||
return rideInfo['carType'] == driverType;
|
||||
case 'Lady':
|
||||
return ['Comfort', 'Speed', 'Lady']
|
||||
.contains(rideInfo['carType']);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
// 1. الفلترة حسب نوع السيارة (تم نقل المنطق للكنترولر، لكن هنا للعرض فقط)
|
||||
// الكنترولر الجديد يفلتر عند الإضافة، لكن لا ضرر من التأكيد هنا
|
||||
final ridesList = controller.availableRides;
|
||||
|
||||
if (filteredRides.isEmpty) {
|
||||
if (ridesList.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"No rides available for your vehicle type.".tr,
|
||||
style: AppStyle.subtitle,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
Icon(CupertinoIcons.car_detailed,
|
||||
size: 60,
|
||||
color:
|
||||
AppColor.primaryColor.withOpacity(0.5)),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"No rides available right now.".tr,
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.getRideAvailable(
|
||||
forceRefresh: true),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text("Refresh Market".tr),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 16),
|
||||
itemCount: filteredRides.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RideAvailableCard(
|
||||
rideInfo: filteredRides[index],
|
||||
);
|
||||
// 2. عرض القائمة
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await controller.getRideAvailable(forceRefresh: true);
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 16),
|
||||
itemCount: ridesList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return RideAvailableCard(
|
||||
rideInfo: ridesList[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// بطاقة الرحلة (The Card)
|
||||
// =============================================================================
|
||||
class RideAvailableCard extends StatelessWidget {
|
||||
final Map<String, dynamic> rideInfo;
|
||||
|
||||
@@ -99,50 +110,45 @@ class RideAvailableCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// The main card with improved styling
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
elevation: 5,
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
// You can add an action here, e.g., show ride details on a map
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRouteInfo(),
|
||||
const Divider(height: 32),
|
||||
_buildRideDetails(),
|
||||
const SizedBox(height: 20),
|
||||
_buildAcceptButton(),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildRouteInfo(),
|
||||
const Divider(height: 24, thickness: 0.5),
|
||||
_buildRideDetails(),
|
||||
const SizedBox(height: 20),
|
||||
_buildAcceptButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Header section with Price and Car Type
|
||||
// ---------------------------------------------------------------------------
|
||||
// تصميم البطاقة (Header, Route, Details)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Fare'.tr, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
Text('${rideInfo['price']} \$',
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 24, color: AppColor.primaryColor)),
|
||||
Text('Price'.tr, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
Text(
|
||||
'${rideInfo['price']} ${'SYP'.tr}', // العملة
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 20, color: AppColor.primaryColor, height: 1.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
@@ -150,48 +156,49 @@ class RideAvailableCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.greenColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColor.greenColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Text(
|
||||
rideInfo['carType'],
|
||||
rideInfo['carType'] ?? 'Fixed Price'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.greenColor, fontSize: 12),
|
||||
.copyWith(color: AppColor.greenColor, fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Visual representation of the pickup and dropoff route
|
||||
Widget _buildRouteInfo() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Dotted line and icons column
|
||||
Column(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.circle_fill,
|
||||
color: AppColor.greenColor, size: 20),
|
||||
...List.generate(
|
||||
4,
|
||||
(index) => Container(
|
||||
height: 4,
|
||||
width: 2,
|
||||
color: AppColor.writeColor,
|
||||
margin: const EdgeInsets.symmetric(vertical: 2),
|
||||
)),
|
||||
const Icon(CupertinoIcons.location_solid,
|
||||
color: Colors.red, size: 20),
|
||||
const Icon(Icons.my_location,
|
||||
color: AppColor.primaryColor, size: 18),
|
||||
Container(
|
||||
height: 30,
|
||||
width: 1,
|
||||
color: Colors.grey.shade300,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
),
|
||||
const Icon(Icons.location_on, color: Colors.red, size: 18),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Location text column
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLocationText(rideInfo['startName'], 'Pickup'.tr),
|
||||
const SizedBox(height: 20),
|
||||
_buildLocationText(rideInfo['endName'], 'Dropoff'.tr),
|
||||
Text(rideInfo['startName'] ?? 'Unknown Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.title.copyWith(fontSize: 14)),
|
||||
const SizedBox(height: 22),
|
||||
Text(rideInfo['endName'] ?? 'Destination'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.title.copyWith(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -199,172 +206,164 @@ class RideAvailableCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Helper for location text
|
||||
Widget _buildLocationText(String location, String label) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
location,
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.normal),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Ride details section with Distance and Passenger Rating
|
||||
Widget _buildRideDetails() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoChip(
|
||||
icon: CupertinoIcons.map_pin_ellipse,
|
||||
value: '${rideInfo['distance']} ${'KM'.tr}',
|
||||
label: 'Distance'.tr,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
_buildInfoChip(
|
||||
icon: CupertinoIcons.star_fill,
|
||||
value: '${rideInfo['passengerRate']}',
|
||||
label: 'Rating'.tr,
|
||||
color: Colors.amber,
|
||||
),
|
||||
_infoItem(Icons.social_distance, '${rideInfo['distance']} KM'),
|
||||
_infoItem(Icons.access_time, '${rideInfo['duration']} Min'),
|
||||
_infoItem(Icons.star, '${rideInfo['passengerRate'] ?? 5.0}',
|
||||
iconColor: Colors.amber),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// A reusable chip for displaying info with an icon
|
||||
Widget _buildInfoChip(
|
||||
{required IconData icon,
|
||||
required String value,
|
||||
required String label,
|
||||
required Color color}) {
|
||||
return Column(
|
||||
Widget _infoItem(IconData icon, String text,
|
||||
{Color iconColor = Colors.grey}) {
|
||||
return Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(value, style: AppStyle.title),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(text, style: AppStyle.subtitle.copyWith(fontSize: 13)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// The accept button with improved styling
|
||||
// ---------------------------------------------------------------------------
|
||||
// زر القبول والمنطق الكامل (Accept Logic) 🔥
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildAcceptButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check_circle_outline, color: Colors.white),
|
||||
label: Text('Accept'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
onPressed: _acceptRide,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: _acceptRideNewLogic,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.greenColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Text(
|
||||
'Accept Ride'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Ride Acceptance Logic ---
|
||||
// This logic is copied exactly from your original code.
|
||||
void _acceptRide() async {
|
||||
var res = await CRUD().post(
|
||||
link: '${AppLink.endPoint}rides/updateStausFromSpeed.php',
|
||||
// 🔥🔥🔥 الوظيفة الأهم: قبول الرحلة وتجهيز البيانات 🔥🔥🔥
|
||||
void _acceptRideNewLogic() async {
|
||||
// 1. إظهار Loading
|
||||
Get.dialog(
|
||||
const Center(child: MyCircularProgressIndicator()),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
try {
|
||||
String driverId = box.read(BoxName.driverID).toString();
|
||||
|
||||
// 2. إرسال الطلب للسيرفر (acceptRide.php الجديد)
|
||||
var response = await CRUD().post(
|
||||
link: "${AppLink.ride}/rides/acceptRide.php",
|
||||
payload: {
|
||||
'id': rideInfo['id'],
|
||||
'rideTimeStart': DateTime.now().toString(),
|
||||
'status': 'Apply',
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
});
|
||||
|
||||
if (res != "failure") {
|
||||
List<String> bodyToPassenger = [
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
box.read(BoxName.tokenDriver).toString(),
|
||||
];
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
await CRUD().postFromDialogue(link: AppLink.addDriverOrder, payload: {
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
'order_id': rideInfo['id'],
|
||||
'status': 'Apply'
|
||||
});
|
||||
// await CRUD().post(link: AppLink.updateRides, payload: {
|
||||
// 'id': rideInfo['id'],
|
||||
// 'DriverIsGoingToPassenger': DateTime.now().toString(),
|
||||
// 'status': 'Applied'
|
||||
// });
|
||||
await CRUD().post(
|
||||
link: AppLink.updateWaitingRide,
|
||||
payload: {'id': rideInfo['id'], 'status': 'Applied'});
|
||||
// if (AppLink.endPoint.toString() != AppLink.seferCairoServer) {
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: rideInfo['passengerToken'].toString(),
|
||||
title: 'Accepted Ride'.tr,
|
||||
body: 'your ride is Accepted'.tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'start',
|
||||
driverList: bodyToPassenger, category: 'Accepted Ride',
|
||||
'id': rideInfo['id'].toString(),
|
||||
'driver_id': driverId,
|
||||
'status': 'Apply', // الحالة المتفق عليها
|
||||
'passengerToken': rideInfo['passengerToken'].toString(),
|
||||
},
|
||||
);
|
||||
Get.back();
|
||||
Get.to(() => PassengerLocationMapPage(), arguments: {
|
||||
'passengerLocation': rideInfo['start_location'].toString(),
|
||||
'passengerDestination': rideInfo['end_location'].toString(),
|
||||
'Duration': rideInfo['duration'].toString(),
|
||||
'totalCost': rideInfo['price'].toString(),
|
||||
'Distance': rideInfo['distance'].toString(),
|
||||
'name': rideInfo['first_name'].toString(),
|
||||
'phone': rideInfo['phone'].toString(),
|
||||
'email': rideInfo['email'].toString(),
|
||||
'WalletChecked': rideInfo['payment_method'].toString(),
|
||||
'tokenPassenger': rideInfo['passengerToken'].toString(),
|
||||
'direction':
|
||||
'https://www.google.com/maps/dir/${rideInfo['start_location']}/${rideInfo['end_location']}/',
|
||||
'DurationToPassenger': rideInfo['duration'].toString(),
|
||||
'rideId': rideInfo['id'].toString(),
|
||||
'passengerId': rideInfo['passenger_id'].toString(),
|
||||
'driverId': box.read(BoxName.driverID).toString(),
|
||||
'durationOfRideValue': rideInfo['duration'].toString(),
|
||||
'paymentAmount': rideInfo['price'].toString(),
|
||||
'paymentMethod': 'cash'.toString() == 'true' ? 'visa' : 'cash',
|
||||
'isHaveSteps': 'startEnd'.toString(),
|
||||
'step0': ''.toString(),
|
||||
'step1': ''.toString(),
|
||||
'step2': ''.toString(),
|
||||
'step3': ''.toString(),
|
||||
'step4': ''.toString(),
|
||||
'passengerWalletBurc': rideInfo['bruc'].toString(),
|
||||
'timeOfOrder': DateTime.now().toString(),
|
||||
'totalPassenger': rideInfo['price'].toString(),
|
||||
'carType': rideInfo['carType'].toString(),
|
||||
'kazan': Get.find<HomeCaptainController>().kazan.toString(),
|
||||
'startNameLocation': rideInfo['startName'].toString(),
|
||||
'endNameLocation': rideInfo['endName'].toString(),
|
||||
});
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
"This ride is already taken by another driver.".tr, '', () {
|
||||
CRUD().post(
|
||||
link: AppLink.deleteAvailableRide, payload: {'id': rideInfo['id']});
|
||||
|
||||
Get.back();
|
||||
});
|
||||
// إخفاء الـ Loading
|
||||
Get.back();
|
||||
|
||||
// 3. تحليل الرد
|
||||
var jsonResponse = jsonDecode(response);
|
||||
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
// ✅ نجاح: أنت الفائز بالرحلة
|
||||
|
||||
// تحديث حالة السائق محلياً
|
||||
Get.find<HomeCaptainController>().changeRideId();
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
|
||||
// 🔥 تجهيز الـ Arguments كاملة (Mapping) 📦
|
||||
// نأخذ البيانات من rideInfo (القادمة من getRideWaiting) ونمررها للخريطة
|
||||
Map<String, dynamic> fullRideArgs = {
|
||||
// معرفات الرحلة
|
||||
'rideId': rideInfo['id'].toString(),
|
||||
'passengerId': rideInfo['passengerId'].toString(),
|
||||
'driverId': driverId,
|
||||
|
||||
// المواقع (يجب أن تكون Strings بصيغة "lat,lng")
|
||||
'passengerLocation': rideInfo['start_location'].toString(),
|
||||
'passengerDestination': rideInfo['end_location'].toString(),
|
||||
|
||||
// الأسماء والعناوين
|
||||
'startNameLocation': rideInfo['startName'].toString(),
|
||||
'endNameLocation': rideInfo['endName'].toString(),
|
||||
|
||||
// تفاصيل الراكب
|
||||
'name': rideInfo['first_name'] ?? 'Passenger',
|
||||
'phone': rideInfo['phone'].toString(),
|
||||
'email': rideInfo['email'] ?? '',
|
||||
'tokenPassenger': rideInfo['passengerToken'].toString(),
|
||||
'passengerWalletBurc': rideInfo['bruc'].toString(), // رصيد الراكب
|
||||
|
||||
// التفاصيل المالية والرحلة
|
||||
'totalCost': rideInfo['price'].toString(), // السعر الكلي
|
||||
'paymentAmount': rideInfo['price'].toString(), // المبلغ المطلوب
|
||||
'Distance': rideInfo['distance'].toString(),
|
||||
'Duration': rideInfo['duration'].toString(),
|
||||
'durationOfRideValue':
|
||||
rideInfo['duration'].toString(), // تكرار للتأكد
|
||||
'carType': rideInfo['carType'].toString(),
|
||||
|
||||
// الدفع والمحفظة
|
||||
'paymentMethod': (rideInfo['payment_method'] == 'visa' ||
|
||||
rideInfo['payment_method'] == 'wallet')
|
||||
? 'visa'
|
||||
: 'cash',
|
||||
'WalletChecked':
|
||||
rideInfo['passenger_wallet'].toString() != '0' ? 'true' : 'false',
|
||||
'kazan': Get.find<HomeCaptainController>()
|
||||
.kazan
|
||||
.toString(), // نسبة الشركة (من الكنترولر)
|
||||
|
||||
// بيانات إضافية (لتجنب الـ Null Safety errors)
|
||||
'direction':
|
||||
'http://googleusercontent.com/maps.google.com/maps?saddr=${rideInfo['start_location']}&daddr=${rideInfo['end_location']}',
|
||||
'timeOfOrder': DateTime.now().toString(),
|
||||
'isHaveSteps': 'false', // لو كان عندك خطوات في الـ waitingRides ضيفها
|
||||
'step0': '', 'step1': '', 'step2': '', 'step3': '', 'step4': '',
|
||||
};
|
||||
|
||||
// حفظ البيانات في الصندوق احتياطياً (Crash Recovery)
|
||||
box.write(BoxName.rideArguments, fullRideArgs);
|
||||
|
||||
// الانتقال لصفحة الخريطة ومسح الصفحات السابقة لضمان عدم الرجوع للسوق
|
||||
Get.offAll(() => PassengerLocationMapPage(), arguments: fullRideArgs);
|
||||
} else {
|
||||
// ❌ فشل: الرحلة أخذها سائق آخر
|
||||
// نقوم بتحديث القائمة فوراً
|
||||
Get.find<RideAvailableController>()
|
||||
.getRideAvailable(forceRefresh: true);
|
||||
|
||||
MyDialog().getDialog(
|
||||
"Trip taken".tr,
|
||||
"This ride was just accepted by another driver.".tr,
|
||||
() => Get.back(), // زر الموافقة
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.back(); // إخفاء اللودينج في حال الخطأ
|
||||
print("Accept Ride Error: $e");
|
||||
MyDialog().getDialog(
|
||||
"Error".tr,
|
||||
"An unexpected error occurred. Please try again.".tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user