26-1-20/1

This commit is contained in:
Hamza-Ayed
2026-01-20 10:11:10 +03:00
parent 374f9e9bf3
commit 3c0ae4cf2f
53 changed files with 89652 additions and 6861 deletions

View File

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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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: () {

View File

@@ -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,

View File

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

View File

@@ -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,

View File

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

View File

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

View File

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

View 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;
}
}

View 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());
}
}

View File

@@ -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,

View File

@@ -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

View File

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

View File

@@ -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;