feat: Gemini AI (doc OCR + face match) + role-assignment endpoints

- GeminiService (integrations/gemini, @Global): extractDocument (OCR fields) + faceMatch; graceful if no GEMINI_API_KEY
- documents: face-match now real via Gemini; POST /admin/documents/:id/ocr extracts fields for CS auto-fill
- roles: PATCH /admin/users/:id/role (admin) + POST /platform/users/role (bootstrap via x-platform-secret)
- config: gemini + platform.secret; .env.example entries

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 20:01:26 +03:00
co-authored by Claude Opus 4.8
parent 04200db438
commit 6234e66471
1541 changed files with 267713 additions and 16 deletions
+101
View File
@@ -0,0 +1,101 @@
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class AboutPage extends StatelessWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'About Us'.tr,
body: [
// Company Logo (consider adding an image asset)
ListView(
children: [
Center(
child: Image.asset(
'assets/images/logo.png', // Replace with your logo image asset path
height: 100.0,
width: 100.0,
),
), // Company Name and Location
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Intaleq LLC\n${'Syria'.tr}',
style: AppStyle.headTitle2,
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'Intaleq is a ride-sharing app designed with your safety and affordability in mind. We connect you with reliable drivers in your area, ensuring a convenient and stress-free travel experience.\n\nHere are some of the key features that set us apart:'
.tr,
style: AppStyle.title,
textAlign: TextAlign.center,
),
), // Security Features List
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
Row(
children: [
const Icon(Icons.lock, color: Colors.blue),
const SizedBox(width: 8.0),
Text(
'Most Secure Methods'.tr,
style: AppStyle.title,
),
],
),
const SizedBox(height: 8.0),
Row(
children: [
const Icon(Icons.phone, color: Colors.blue),
const SizedBox(width: 8.0),
Text(
'In-App VOIP Calls'.tr,
style: AppStyle.title,
),
],
),
const SizedBox(height: 8.0),
Row(
children: [
const Icon(Icons.videocam, color: Colors.blue),
const SizedBox(width: 8.0),
Text(
'Recorded Trips for Safety'.tr,
style: AppStyle.title,
),
],
),
],
),
), // Affordability Highlight
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'\nWe also prioritize affordability, offering competitive pricing to make your rides accessible.'
.tr,
style: AppStyle.title,
textAlign: TextAlign.center,
),
),
],
),
// About Us Text
],
isleading: true);
}
}
@@ -0,0 +1,145 @@
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../constant/style.dart';
class FrequentlyQuestionsPage extends StatelessWidget {
const FrequentlyQuestionsPage({super.key});
@override
Widget build(BuildContext context) {
String selectedPayment = 'cash'; // Replace with your initial selection
bool canCancelRide = false;
return MyScafolld(
title: 'Frequently Questions'.tr,
body: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: [
// Question 1: How do I request a ride?
ExpansionTile(
title: Text(
'How do I request a ride?'.tr,
style: AppStyle.title,
),
children: [
Text(
'Step-by-step instructions on how to request a ride through the Intaleq app.'
.tr,
style: AppStyle.title,
),
],
),
// Question 2: What types of vehicles are available?
ExpansionTile(
title: Text(
'What types of vehicles are available?'.tr,
style: AppStyle.title,
),
children: [
Text(
'Intaleq offers a variety of vehicle options to suit your needs, including economy, comfort, and luxury. Choose the option that best fits your budget and passenger count.'
.tr,
style: AppStyle.title,
),
],
),
// Question 3: How can I pay for my ride?
ExpansionTile(
title: Text(
'How can I pay for my ride?'.tr,
style: AppStyle.title,
),
children: [
Text(
'Intaleq offers multiple payment methods for your convenience. Choose between cash payment or credit/debit card payment during ride confirmation.'
.tr,
style: AppStyle.title,
),
],
),
// Question 4: Can I cancel my ride? (if applicable)
ExpansionTile(
title: Text(
'Can I cancel my ride?'.tr,
style: AppStyle.title,
),
children: [
Text(
'Yes, you can cancel your ride under certain conditions (e.g., before driver is assigned). See the Intaleq cancellation policy for details.'
.tr,
style: AppStyle.title,
),
],
),
// Question 5 & 6: Driver-specific questions
ExpansionTile(
title: Text(
'Driver Registration & Requirements'.tr,
style: AppStyle.title,
),
children: [
Text(
'${'How can I register as a driver?'.tr}\n${'What are the requirements to become a driver?'.tr}',
style: AppStyle.title,
),
InkWell(
onTap: () {
MyDialog().getDialog('title', 'midTitle', () {
; //todo add in this dialog papers for driver
});
},
child: Text(
'Visit our website or contact Intaleq support for information on driver registration and requirements.'
.tr,
style: AppStyle.title,
),
),
],
),
// Question 7: How do I communicate with the other party?
ExpansionTile(
title: Text(
'How do I communicate with the other party (passenger/driver)?'
.tr,
style: AppStyle.title,
),
children: [
Text(
'Intaleq provides in-app chat functionality to allow you to communicate with your driver or passenger during your ride.'
.tr,
style: AppStyle.title,
),
],
),
// Question 8: What safety measures does Intaleq offer?
ExpansionTile(
title: Text(
'What safety measures does Intaleq offer?'.tr,
style: AppStyle.title,
),
children: [
Text(
'Intaleq prioritizes your safety. We offer features like driver verification, in-app trip tracking, and emergency contact options.'
.tr,
style: AppStyle.title,
),
],
),
],
),
)
],
isleading: true);
}
}
@@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/controller/profile/setting_controller.dart';
import 'package:sefer_driver/views/lang/languages.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
// تأكد من صحة مسارات الاستيراد هذه
import '../../../../controller/functions/vibrate.dart'; // Controller with isVibrate
import '../../../auth/country_widget.dart';
import 'about_us.dart';
import 'frequantly_question.dart';
import 'using_app_page.dart';
class SettingsCaptain extends StatelessWidget {
const SettingsCaptain({super.key});
@override
Widget build(BuildContext context) {
// تحميل الـ Controllers المطلوبة
final settingsController = Get.put(SettingController());
final homeController = Get.put(HomePageController());
return MyScafolld(
title: 'Settings'.tr,
isleading: true,
body: [
ListView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
children: <Widget>[
// --- القسم الأول: عام ---
_buildSectionHeader('General'.tr, context),
_buildSettingsCard(
children: [
_buildListTile(
icon: Icons.language_outlined,
title: 'Language'.tr,
subtitle: 'Change the app language'.tr,
onTap: () => Get.to(() => const Language()),
),
// _buildListTile(
// icon: Icons.flag_outlined,
// title: 'Change Country'.tr,
// subtitle: 'Get features for your country'.tr,
// onTap: () => Get.to(
// () => MyScafolld(
// title: 'Change Country'.tr,
// body: [CountryPickerFromSetting()],
// isleading: true,
// ),
// ),
// ),
],
),
const SizedBox(height: 20),
// --- القسم الثاني: تفضيلات التطبيق ---
_buildSectionHeader('App Preferences'.tr, context),
_buildSettingsCard(
children: [
_buildSwitchTile(
icon: Icons.map_outlined,
color: AppColor.redColor,
title: 'Google Map App'.tr,
subtitle: 'Run Google Maps directly'.tr,
controller: settingsController,
valueGetter: (ctrl) => (ctrl).isGoogleMapsEnabled,
onChanged: (ctrl) => (ctrl).onChangMapApp(),
),
_buildSwitchTile(
icon: Icons.vibration,
title: 'Vibration'.tr,
subtitle: 'Vibration feedback for buttons'.tr,
controller: homeController,
valueGetter: (ctrl) => (ctrl).isVibrate,
onChanged: (ctrl) => (ctrl)
.changeVibrateOption(true), // قد تحتاج لتعديل الدالة
),
],
),
const SizedBox(height: 20),
// --- القسم الثالث: المساعدة والدعم ---
_buildSectionHeader('Help & Support'.tr, context),
_buildSettingsCard(
children: [
_buildListTile(
icon: Icons.quiz_outlined,
title: 'Frequently Questions'.tr,
onTap: () => Get.to(() => const FrequentlyQuestionsPage()),
),
_buildListTile(
icon: Icons.support_agent,
title: "How to use App".tr,
onTap: () => Get.to(() => const UsingAppPage()),
),
_buildListTile(
icon: Icons.info_outline,
title: 'About Us'.tr,
onTap: () => Get.to(() => const AboutPage()),
),
],
),
const SizedBox(height: 20),
// --- القسم الرابع: تسجيل الخروج ---
_buildSectionHeader('Account'.tr, context),
_buildSettingsCard(
children: [
ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: Text(
'Logout'.tr,
style: const TextStyle(
color: Colors.red, fontWeight: FontWeight.w500),
),
onTap: () {
MyDialog().getDialog(
'Logout'.tr,
'Are you sure you want to logout?'.tr,
() {
// أضف دالة تسجيل الخروج هنا
Get.back(); // لإغلاق مربع الحوار
},
// isConfirmation: true,
);
},
)
],
)
],
),
],
);
}
// ويدجت لبناء عنوان كل قسم
Widget _buildSectionHeader(String title, BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 12.0),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.grey.shade600,
fontWeight: FontWeight.bold,
),
),
);
}
// ويدجت لبناء بطاقة الإعدادات
Widget _buildSettingsCard({required List<Widget> children}) {
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
clipBehavior: Clip.antiAlias, // مهم لجعل splash effect داخل حدود البطاقة
child: Column(children: children),
);
}
// ويدجت لبناء عنصر قابل للضغط (مثل اللغة، عن التطبيق)
Widget _buildListTile({
required IconData icon,
required String title,
String? subtitle,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon, color: Colors.grey.shade700),
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: subtitle != null ? Text(subtitle) : null,
trailing:
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
onTap: onTap,
);
}
// ويدجت لبناء عنصر يحتوي على مفتاح تفعيل/إلغاء (Switch)
Widget _buildSwitchTile<T extends GetxController>({
required IconData icon,
Color? color,
required String title,
required String subtitle,
required T controller,
required bool Function(T) valueGetter,
required Function(T) onChanged,
}) {
return GetBuilder<T>(
init: controller,
builder: (ctrl) {
return SwitchListTile(
secondary: Icon(icon, color: color ?? Colors.grey.shade700),
title:
Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(subtitle),
value: valueGetter(ctrl),
onChanged: (value) => onChanged(ctrl),
activeColor: AppColor.primaryColor,
);
},
);
}
}
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
// 1. إنشاء Class لتمثيل بيانات كل سؤال وجواب
class FaqItem {
final String question;
final Widget answer; // استخدام Widget يسمح بوضع نصوص أو صور
final IconData icon;
FaqItem({required this.question, required this.answer, required this.icon});
}
class UsingAppPage extends StatelessWidget {
const UsingAppPage({super.key});
@override
Widget build(BuildContext context) {
// 2. تجهيز قائمة البيانات بشكل منظم
final List<FaqItem> faqItems = [
FaqItem(
question: "What are the order details we provide to you?".tr,
icon: Icons.receipt_long_outlined,
answer: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.network(
'https://api.tripz-egypt.com/tripz/imageForUsingApp/order_page.jpg',
fit: BoxFit.cover,
// يمكنك إضافة مؤشر تحميل هنا
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const Center(child: CircularProgressIndicator());
},
errorBuilder: (context, error, stackTrace) {
return const Center(
child:
Icon(Icons.error_outline, color: Colors.red, size: 40));
},
),
),
),
FaqItem(
question: "What is the feature of our wallet?".tr,
icon: Icons.account_balance_wallet_outlined,
answer: Text(
'''Intaleq Wallet Features:
- Transfer money multiple times.
- Transfer to anyone.
- Make purchases.
- Charge your account.
- Charge a friend's Intaleq account.
- Store your money with us and receive it in your bank as a monthly salary.'''
.tr,
style:
TextStyle(fontSize: 15, height: 1.5, color: Colors.grey.shade700),
),
),
FaqItem(
question: "What is Types of Trips in Intaleq?".tr,
icon: Icons.map_outlined,
answer: Text(
'''Types of Trips in Intaleq:
- Comfort: For cars newer than 2017 with air conditioning.
- Lady: For girl drivers.
- Speed: For fixed salary and endpoints.
- Mashwari: For flexible trips where passengers choose the car and driver with prior arrangements.
- Raih Gai: For same-day return trips longer than 50km.'''
.tr,
style:
TextStyle(fontSize: 15, height: 1.5, color: Colors.grey.shade700),
),
),
];
// 3. بناء الواجهة الرسومية باستخدام البيانات
return MyScafolld(
title: "How to use App".tr, // تم تغيير العنوان ليكون أعم
isleading: true,
body: [
ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
itemCount: faqItems.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final item = faqItems[index];
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16)),
clipBehavior: Clip.antiAlias,
child: ExpansionTile(
leading: Icon(item.icon, color: Theme.of(context).primaryColor),
title: Text(item.question,
style: const TextStyle(fontWeight: FontWeight.w600)),
childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1),
const SizedBox(height: 12),
item.answer,
],
),
);
},
),
],
);
}
}
+193
View File
@@ -0,0 +1,193 @@
import 'package:sefer_driver/constant/info.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:video_player/video_player.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../../../../controller/home/captin/help/video_controller.dart';
import 'package:flutter/cupertino.dart';
class VideoListPage extends StatelessWidget {
final VideoController videoController = Get.put(VideoController());
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Videos Tutorials'.tr),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 10.0), // Outer padding around the list
child: GetBuilder<VideoController>(
builder: (videoController) {
return ListView.builder(
itemCount: videoController.videos.length,
itemBuilder: (context, index) {
final video = videoController.videos[index];
return Container(
margin: const EdgeInsets.symmetric(
vertical: 8.0), // Spacing between each card
decoration: BoxDecoration(
color: CupertinoColors.white,
borderRadius:
BorderRadius.circular(12.0), // Rounded corners
boxShadow: [
BoxShadow(
color: CupertinoColors.systemGrey.withOpacity(0.3),
offset: const Offset(
0, 4), // Offset for shadow to appear below
blurRadius: 10.0, // Blur for softer shadow effect
),
],
border: Border.all(
color: CupertinoColors.systemGrey4,
width: 0.5, // Subtle border for a refined iOS-like look
),
),
child: CupertinoListTile(
title: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
video['title'],
style: const TextStyle(
color: CupertinoColors.black,
fontWeight: FontWeight.bold,
),
),
),
subtitle: Text(
video['description'],
style: const TextStyle(
color: CupertinoColors.systemGrey,
),
),
onTap: () {
// Navigate to video player page (iOS-style)
Get.to(() => VideoPlayerPage1(videoUrl: video['url']));
},
),
);
},
);
},
),
),
),
);
}
}
class VideoPlayerPage extends StatelessWidget {
final String videoUrl;
VideoPlayerPage({required this.videoUrl});
final VideoController videoController = Get.put(VideoController());
@override
Widget build(BuildContext context) {
// Initialize the video when the page is loaded
videoController.initializeVideo(videoUrl);
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text(AppInformation.appName),
),
child: SafeArea(
child: Center(
child: GetBuilder<VideoController>(
builder: (controller) {
if (!controller.videoPlayerController.value.isInitialized) {
return const CircularProgressIndicator(); // Show loading indicator while initializing
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Video player widget
AspectRatio(
aspectRatio:
controller.videoPlayerController.value.aspectRatio,
// child: VideoPlayer(controller.videoPlayerController),
child: VideoPlayer(controller.videoPlayerController),
),
const SizedBox(height: 20),
// Play/pause button
CupertinoButton(
onPressed: () {
if (controller.videoPlayerController.value.isPlaying) {
controller.pause();
} else {
controller.play();
}
},
child: Icon(
controller.videoPlayerController.value.isPlaying
? CupertinoIcons.pause
: CupertinoIcons.play_arrow,
color: CupertinoColors.activeBlue,
size: 30,
),
),
],
);
},
),
),
),
);
}
}
class VideoPlayerPage1 extends StatelessWidget {
final String videoUrl;
const VideoPlayerPage1({required this.videoUrl});
@override
Widget build(BuildContext context) {
// Extract the video ID from the URL
String videoId = YoutubePlayer.convertUrlToId(videoUrl)!;
// Create a YoutubePlayerController
final YoutubePlayerController _controller = YoutubePlayerController(
initialVideoId: videoId,
flags: const YoutubePlayerFlags(
autoPlay: true,
loop: true,
mute: false,
captionLanguage: 'ar',
),
);
return Scaffold(
body: Stack(
children: [
// Full-screen YouTube player
Positioned.fill(
child: YoutubePlayer(
controller: _controller,
showVideoProgressIndicator: true,
),
),
// Overlay back button in the top left corner for exit
Positioned(
top: 40.0,
left: 16.0,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
),
),
],
),
);
}
}
@@ -0,0 +1,231 @@
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../controller/home/captin/help/assurance_controller.dart';
import 'package:flutter/cupertino.dart';
class AssuranceHealthPage extends StatelessWidget {
AssuranceHealthPage({super.key});
AssuranceHealthController assuranceHealthController =
Get.put(AssuranceHealthController());
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("Health Insurance".tr),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GetBuilder<AssuranceHealthController>(
builder: (assuranceHealthController) {
return Column(
children: [
Text(
"When you complete 500 trips, you will be eligible for exclusive health insurance offers."
.tr,
textAlign: TextAlign.center,
style: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
CupertinoButton.filled(
child: Text("Show My Trip Count".tr),
onPressed: () async {
assuranceHealthController.getTripCountByCaptain();
},
),
_buildTripCountAvatar(
assuranceHealthController.tripCount['count'] == null
? '0'
: assuranceHealthController.tripCount['count']
.toString(),
),
],
),
const SizedBox(height: 10),
Container(
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Text(
"We have partnered with health insurance providers to offer you special health coverage. Complete 500 trips and receive a 20% discount on health insurance premiums."
.tr,
style: CupertinoTheme.of(context)
.textTheme
.textStyle
.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 10),
CupertinoButton.filled(
disabledColor: AppColor.blueColor,
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
borderRadius: BorderRadius.circular(12),
child: Text(
"Would you like to proceed with health insurance?".tr,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
onPressed: () async {
// Show confirmation dialog before proceeding
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
// Variable to store the health insurance provider chosen by the driver
TextEditingController providerController =
TextEditingController();
return CupertinoAlertDialog(
title: Text(
"Confirmation".tr,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
),
content: Column(
children: [
Text(
"Would you like to proceed with health insurance?"
.tr,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 20),
CupertinoTextField(
controller: providerController,
placeholder:
"Do you have a disease for a long time?".tr,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(
color: CupertinoColors.systemGrey,
width: 1),
borderRadius: BorderRadius.circular(8),
),
),
],
),
actions: <Widget>[
CupertinoDialogAction(
isDefaultAction: true,
child: Text(
"Yes".tr,
style: const TextStyle(
color: CupertinoColors.activeBlue),
),
onPressed: () async {
// Ensure the provider name is not empty
if (providerController.text.isNotEmpty) {
// Call the function to insert data into the database
await assuranceHealthController
.addDriverHealthAssurance(
healthInsuranceProvider:
providerController.text,
);
// Close the dialog and navigate to a success screen or show a snackbar
Navigator.of(context).pop();
} else {
// Show an alert if the provider name is empty
showCupertinoDialog(
context: context,
builder: (_) => CupertinoAlertDialog(
title: Text("Error".tr),
content: Text(
"Do you have a disease for a long time?"
.tr),
actions: [
CupertinoDialogAction(
child: Text("OK".tr),
onPressed: () =>
Navigator.of(context).pop(),
),
],
),
);
}
},
),
CupertinoDialogAction(
child: Text(
"No".tr,
style: const TextStyle(
color: CupertinoColors.destructiveRed),
),
onPressed: () {
Navigator.of(context)
.pop(); // Just close the dialog
// Optionally show feedback if the driver opts out
// Get.snackbar(
// "Opted out".tr,
// "You have chosen not to proceed with health insurance."
// .tr,
// backgroundColor:
// CupertinoColors.systemGrey);
mySnackeBarError(
"You have chosen not to proceed with health insurance."
.tr);
},
),
],
);
},
);
},
),
],
);
}),
),
),
);
}
Widget _buildTripCountAvatar(String count) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const RadialGradient(
colors: [
Color(0xFF42A5F5),
Color(0xFF1976D2),
], // Health theme colors
center: Alignment.center,
radius: 0.8,
),
boxShadow: [
BoxShadow(
color: CupertinoColors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: Text(
count,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: CupertinoColors.white,
),
),
),
);
}
}
+119
View File
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/colors.dart';
class BottomBarController extends GetxController {
var currentIndex = 0.obs;
void changePage(int index) {
currentIndex.value = index;
}
}
class HomeScreen extends StatelessWidget {
final BottomBarController controller = Get.put(BottomBarController());
HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Bottom Bar Example'.tr),
),
body: Obx(() => IndexedStack(
index: controller.currentIndex.value,
children: const [
HomeView(),
ProfileView(),
StatisticsView(),
WalletView(),
],
)),
bottomNavigationBar: Obx(() => BottomNavigationBar(
backgroundColor: Colors.greenAccent,
currentIndex: controller.currentIndex.value,
onTap: controller.changePage,
items: [
BottomNavigationBarItem(
icon: const Icon(
Icons.home,
color: AppColor.primaryColor,
),
label: 'Home'.tr,
),
BottomNavigationBarItem(
icon: const Icon(
Icons.person,
color: AppColor.primaryColor,
),
label: 'Profile'.tr,
),
BottomNavigationBarItem(
icon: const Icon(
Icons.bar_chart,
color: AppColor.primaryColor,
),
label: 'Statistics'.tr,
),
BottomNavigationBarItem(
icon: const Icon(
Icons.account_balance_wallet,
color: AppColor.primaryColor,
),
label: 'Wallet'.tr,
),
],
)),
);
}
}
class HomeView extends StatelessWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context) {
Map<String, dynamic> data;
return const Center(
child: Column(
children: [
Text('Home View'),
],
),
);
}
}
class ProfileView extends StatelessWidget {
const ProfileView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Profile View'),
);
}
}
class StatisticsView extends StatelessWidget {
const StatisticsView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Statistics View'),
);
}
}
class WalletView extends StatelessWidget {
const WalletView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Wallet View'),
);
}
}
+303
View File
@@ -0,0 +1,303 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/controller/functions/camer_controller.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
class CameraWidgetCardId extends StatelessWidget {
final CameraClassController cameraClassController =
Get.put(CameraClassController());
CameraWidgetCardId({super.key});
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'Scan Id'.tr,
body: [
Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: GetBuilder<CameraClassController>(
builder: (cameraClassController) =>
cameraClassController.isCameraInitialized
? Stack(
children: [
Container(
decoration: AppStyle.boxDecoration,
child: FittedBox(
fit: BoxFit.fitWidth,
child: SizedBox(
width: Get.width * .9,
height: Get.width *
.9, // Set the desired aspect ratio here
child: CameraPreview(
cameraClassController.cameraController,
),
),
),
),
Positioned(
top: 72,
child: Container(
width: 230,
height: 25,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.yellowColor,
width: 2),
),
),
),
Positioned(
top: 60,
right: 5,
child: Container(
width: 230,
height: 25,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
Positioned(
top: 156,
right: 5,
child: Container(
width: 140,
height: 20,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
Positioned(
top: 175,
right: 5,
child: Container(
width: 140,
height: 15,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
Positioned(
top: 191,
right: 5,
child: Container(
width: 140,
height: 15,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
Positioned(
top: 207,
right: 5,
child: Container(
width: 140,
height: 15,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
Positioned(
top: 225,
right: 5,
child: Container(
width: 140,
height: 15,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
Positioned(
top: 115,
left: 25,
child: Container(
width: 120,
height: 110,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
],
)
: Container(
decoration: AppStyle.boxDecoration,
height: Get.width * 3 / 4,
width: Get.width,
child: Center(
child: Text(
'Camera not initilaized yet'.tr,
style: AppStyle.title,
),
),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
title: 'Scan ID MklGoogle'.tr, onPressed: () {}),
// cameraClassController.takePictureAndMLGoogleScan()),
MyElevatedButton(
title: 'Scan ID Tesseract'.tr, onPressed: () {}),
],
),
MyElevatedButton(
title: 'Scan ID Api'.tr,
onPressed: () => cameraClassController.extractCardId()),
GetBuilder<CameraClassController>(
builder: (cameraClassController) => Expanded(
child:
Text(cameraClassController.scannedText.toString())))
],
)
],
isleading: true);
}
}
class CameraWidgetPassPort extends StatelessWidget {
final CameraClassController cameraClassController =
Get.put(CameraClassController());
CameraWidgetPassPort({super.key});
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'Scan Id'.tr,
body: [
Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: GetBuilder<CameraClassController>(
builder: (cameraClassController) =>
cameraClassController.isCameraInitialized
? Stack(
children: [
Container(
decoration: AppStyle.boxDecoration,
child: FittedBox(
fit: BoxFit.fitWidth,
child: SizedBox(
width: Get.width * .9,
height: Get.width *
.9, // Set the desired aspect ratio here
child: CameraPreview(
cameraClassController.cameraController,
),
),
),
),
Positioned(
top: 35,
left: 35,
width: Get.width * .77,
height:
17, //left":95.0,"top":134.0,"width":2909.0,"height":175.0
child: Container(
// width: 230,
// height: 25,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.yellowColor,
width: 2),
),
),
),
Positioned(
top: 60,
right: 25,
width: 90,
height: 25,
child: Container(
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
Positioned(
top: 110,
right: 90,
child: Container(
width: 140,
height: 20,
decoration: BoxDecoration(
// color: AppColor.blueColor,
border: Border.all(
color: AppColor.blueColor, width: 2),
),
),
),
],
)
: Container(
decoration: AppStyle.boxDecoration,
height: Get.width * 3 / 4,
width: Get.width,
child: Center(
child: Text(
'Camera not initilaized yet',
style: AppStyle.title,
),
),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
title: 'Scan ID MklGoogle'.tr, onPressed: () => {}),
// cameraClassController.takePictureAndMLGoogleScan()),
MyElevatedButton(
title: 'Scan ID Tesseract'.tr, onPressed: () {}),
],
),
MyElevatedButton(
title: 'Scan ID Api'.tr,
onPressed: () => cameraClassController.extractCardId()),
GetBuilder<CameraClassController>(
builder: (cameraClassController) => Expanded(
child:
Text(cameraClassController.scannedText.toString())))
],
)
],
isleading: true);
}
}
+448
View File
@@ -0,0 +1,448 @@
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:sefer_driver/controller/home/captin/map_driver_controller.dart';
import '../../../constant/colors.dart';
import '../../../controller/functions/location_controller.dart';
import '../../../main.dart';
import '../../Rate/rate_passenger.dart';
import '../../widgets/my_textField.dart';
import 'mapDriverWidgets/driver_end_ride_bar.dart';
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});
final LocationController locationController = Get.put(LocationController());
final MapDriverController mapDriverController =
Get.put(MapDriverController());
// دالة ديالوج الخروج
Future<bool> showExitDialog() async {
bool? result = await Get.defaultDialog(
title: "Warning".tr,
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
middleText:
"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)),
cancel: MyElevatedButton(
title: 'Exit'.tr,
kolor: AppColor.redColor,
onPressed: () => Get.back(result: true)),
);
return result ?? false;
}
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
mapDriverController.argumentLoading();
mapDriverController.startTimerToShowPassengerInfoWindowFromDriver();
// 2. فرض التحديث لكل المعرفات (IDs) لضمان ظهورها
// لأن argumentLoading قد تستدعي update() العادية التي لا تؤثر على هؤلاء
mapDriverController
.update(['PassengerInfo', 'DriverEndBar', 'SosConnect']);
}
});
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldExit = await showExitDialog();
if (shouldExit) Get.back();
},
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
// 1. الخريطة (الخلفية)
Positioned.fill(
child: GoogleDriverMap(locationController: locationController)),
// 2. واجهة المستخدم (فوق الخريطة)
SafeArea(
child: Stack(
children: [
// أ) زر الإلغاء (أعلى اليسار)
CancelWidget(mapDriverController: mapDriverController),
// ب) شريط إنهاء الرحلة (أعلى الوسط)
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(), // ويدجت نظيفة
],
),
);
},
),
),
],
),
),
// 3. النوافذ المنبثقة (Overlay)
const PricesWindow(),
],
),
),
);
}
}
// ---------------------------------------------------------------------------
// 1. ويدجت شريط التعليمات (InstructionsOfRoads)
// ---------------------------------------------------------------------------
class InstructionsOfRoads extends StatelessWidget {
const InstructionsOfRoads({super.key});
@override
Widget build(BuildContext context) {
return Positioned(
bottom: 20,
left: 15,
right: 15,
child: GetBuilder<MapDriverController>(
builder: (controller) {
// إخفاء الشريط إذا لم يكن هناك تعليمات
if (controller.currentInstruction.isEmpty) return const SizedBox();
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,
),
),
),
),
],
),
),
),
);
},
);
},
),
);
}
}
// ---------------------------------------------------------------------------
// 2. ويدجت زر الإلغاء (CancelWidget) - كامل
// ---------------------------------------------------------------------------
class CancelWidget extends StatelessWidget {
const CancelWidget({super.key, required this.mapDriverController});
final MapDriverController mapDriverController;
@override
Widget build(BuildContext context) {
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.85,
padding: const EdgeInsets.all(30),
decoration: BoxDecoration(
color: Colors.white,
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'.tr,
style: AppStyle.headTitle2
.copyWith(fontSize: 18, color: Colors.grey[600]),
),
const SizedBox(height: 10),
Text(
'${controller.totalCost} ${'\$'.tr}',
style: AppStyle.headTitle2.copyWith(
color: Colors.black87,
fontSize: 42,
fontWeight: FontWeight.w900,
),
),
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
});
},
),
),
],
),
),
);
},
),
),
);
});
}
}
@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import '../../../../controller/auth/captin/history_captain.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
class HistoryCaptain extends StatelessWidget {
const HistoryCaptain({super.key});
@override
Widget build(BuildContext context) {
Get.put(HistoryCaptainController());
return Scaffold(
backgroundColor: Colors.grey[100], // A softer background color
appBar: AppBar(
title: Text('Ride History'.tr),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 1,
),
body: GetBuilder<HistoryCaptainController>(
builder: (controller) {
if (controller.isloading) {
return const Center(child: CircularProgressIndicator());
}
if (controller.historyData['message'].isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history_toggle_off,
size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No Rides Yet'.tr,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: Colors.grey[600]),
),
],
),
);
}
// 动画: Wrap ListView with AnimationLimiter for staggered animations
return AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: controller.historyData['message'].length,
itemBuilder: (BuildContext context, int index) {
var trip = controller.historyData['message'][index];
// 动画: Apply animation to each list item
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _AnimatedHistoryCard(
trip: trip,
onTap: () {
// Your original logic is preserved here
if (trip['status'] != 'Cancel') {
controller.getHistoryDetails(trip['order_id']);
} else {
MyDialog().getDialog(
'This Trip Was Cancelled'.tr,
'This Trip Was Cancelled'.tr,
() => Get.back(),
);
}
},
),
),
),
);
},
),
);
},
),
);
}
}
// 动画: A new stateful widget to handle the tap animation
class _AnimatedHistoryCard extends StatefulWidget {
final Map<String, dynamic> trip;
final VoidCallback onTap;
const _AnimatedHistoryCard({required this.trip, required this.onTap});
@override
__AnimatedHistoryCardState createState() => __AnimatedHistoryCardState();
}
class __AnimatedHistoryCardState extends State<_AnimatedHistoryCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
_controller.forward();
}
void _onTapUp(TapUpDetails details) {
_controller.reverse();
widget.onTap();
}
void _onTapCancel() {
_controller.reverse();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: ScaleTransition(
scale: _scaleAnimation,
child: Card(
elevation: 4,
shadowColor: Colors.black.withOpacity(0.1),
margin: const EdgeInsets.only(bottom: 16.0),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.receipt_long,
color: Theme.of(context).primaryColor, size: 40),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'OrderId'.tr}: ${widget.trip['order_id']}',
style:
Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
widget.trip['created_at'],
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
const SizedBox(width: 16),
_buildStatusChip(widget.trip['status']),
],
),
),
),
),
);
}
}
// 🎨 A separate function for the status chip, slightly restyled for Material
Widget _buildStatusChip(String status) {
Color chipColor;
Color textColor;
String statusText = status;
IconData iconData;
switch (status) {
case 'Apply':
chipColor = Colors.green.shade50;
textColor = Colors.green.shade800;
iconData = Icons.check_circle;
break;
case 'Refused':
chipColor = Colors.red.shade50;
textColor = Colors.red.shade800;
iconData = Icons.cancel;
break;
case 'Cancel':
chipColor = Colors.orange.shade50;
textColor = Colors.orange.shade800;
iconData = Icons.info;
statusText = 'Cancelled';
break;
default:
chipColor = Colors.grey.shade200;
textColor = Colors.grey.shade800;
iconData = Icons.hourglass_empty;
}
return Chip(
avatar: Icon(iconData, color: textColor, size: 16),
label: Text(
statusText.tr,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
),
backgroundColor: chipColor,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
side: BorderSide.none,
);
}
@@ -0,0 +1,369 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:sefer_driver/controller/auth/captin/history_captain.dart';
import 'package:sefer_driver/controller/functions/launch.dart';
class HistoryDetailsPage extends StatefulWidget {
const HistoryDetailsPage({super.key});
@override
State<HistoryDetailsPage> createState() => _HistoryDetailsPageState();
}
class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
// Get the controller instance
final HistoryCaptainController controller =
Get.find<HistoryCaptainController>();
// Helper method to safely parse LatLng from a string 'lat,lng'
LatLng? _parseLatLng(String? latLngString) {
if (latLngString == null) return null;
final parts = latLngString.split(',');
if (parts.length != 2) return null;
final lat = double.tryParse(parts[0]);
final lng = double.tryParse(parts[1]);
if (lat == null || lng == null) return null;
return LatLng(lat, lng);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
title: Text('Trip Details'.tr),
backgroundColor: Colors.white,
elevation: 1,
),
body: GetBuilder<HistoryCaptainController>(
builder: (controller) {
if (controller.isloading) {
return const Center(child: CircularProgressIndicator());
}
final res = controller.historyDetailsData['data'];
if (res == null) {
return Center(child: Text('Could not load trip details.'.tr));
}
final startLocation = _parseLatLng(res['start_location']);
final endLocation = _parseLatLng(res['end_location']);
// Create markers for the map
final Set<Marker> markers = {};
if (startLocation != null) {
markers.add(Marker(
markerId: const MarkerId('start'),
position: startLocation,
infoWindow: InfoWindow(title: 'Start'.tr)));
}
if (endLocation != null) {
markers.add(Marker(
markerId: const MarkerId('end'),
position: endLocation,
infoWindow: InfoWindow(title: 'End'.tr)));
}
return AnimationLimiter(
child: ListView(
padding: const EdgeInsets.all(16.0),
children: AnimationConfiguration.toStaggeredList(
duration: const Duration(milliseconds: 375),
childAnimationBuilder: (widget) => SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(child: widget),
),
children: [
// --- Map Card ---
_buildMapCard(context, startLocation, endLocation, markers),
const SizedBox(height: 16),
// --- Trip Info Card ---
_DetailCard(
icon: Icons.receipt_long,
title: 'Trip Info'.tr,
child: Column(
children: [
_InfoTile(
label: 'Order ID'.tr,
value: res['id']?.toString() ?? 'N/A'),
_InfoTile(
label: 'Date'.tr,
value: res['date']?.toString() ?? 'N/A'),
],
),
),
// --- Earnings Card ---
_DetailCard(
icon: Icons.account_balance_wallet,
title: 'Earnings & Distance'.tr,
child: Column(
children: [
_InfoTile(
label: 'Your Earnings'.tr,
value: '${res['price_for_driver']}'),
_InfoTile(
label: 'Distance'.tr,
value: '${res['distance']} KM'),
],
),
),
// --- Timeline Card ---
_DetailCard(
icon: Icons.timeline,
title: 'Trip Timeline'.tr,
child: Column(
children: [
_InfoTile(
label: 'Time to Passenger'.tr,
value: res['DriverIsGoingToPassenger'] ?? 'N/A'),
_InfoTile(
label: 'Trip Started'.tr,
value: res['rideTimeStart'] ?? 'N/A'),
_InfoTile(
label: 'Trip Finished'.tr,
value: res['rideTimeFinish'] ?? 'N/A'),
],
),
),
// --- Passenger & Status Card ---
_DetailCard(
icon: Icons.person,
title: 'Passenger & Status'.tr,
child: Column(
children: [
_InfoTile(
label: 'Passenger Name'.tr,
value:
'${res['passengerName']} ${res['last_name']}'),
_InfoTile(
label: 'Status'.tr,
value: res['status'] ?? 'N/A',
isStatus: true),
],
),
),
],
),
),
);
},
),
);
}
Widget _buildMapCard(BuildContext context, LatLng? startLocation,
LatLng? endLocation, Set<Marker> markers) {
// A fallback position if locations are not available
final initialCameraPosition = (startLocation != null)
? CameraPosition(target: startLocation, zoom: 14)
: const CameraPosition(
target: LatLng(31.96, 35.92), zoom: 12); // Fallback to Amman
return Card(
elevation: 4,
shadowColor: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior:
Clip.antiAlias, // Ensures the map respects the border radius
child: Stack(
children: [
SizedBox(
height: 250,
child: GoogleMap(
initialCameraPosition: initialCameraPosition,
markers: markers,
polylines: {
if (startLocation != null && endLocation != null)
Polyline(
polylineId: const PolylineId('route'),
points: [startLocation, endLocation],
color: Colors.deepPurple,
width: 5,
),
},
onMapCreated: (GoogleMapController mapController) {
// Animate camera to fit the route
if (startLocation != null && endLocation != null) {
LatLngBounds bounds = LatLngBounds(
southwest: LatLng(
startLocation.latitude < endLocation.latitude
? startLocation.latitude
: endLocation.latitude,
startLocation.longitude < endLocation.longitude
? startLocation.longitude
: endLocation.longitude,
),
northeast: LatLng(
startLocation.latitude > endLocation.latitude
? startLocation.latitude
: endLocation.latitude,
startLocation.longitude > endLocation.longitude
? startLocation.longitude
: endLocation.longitude,
),
);
mapController.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 60.0));
}
},
),
),
Positioned(
top: 10,
right: 10,
child: FloatingActionButton.small(
heroTag: 'open_maps',
onPressed: () {
if (startLocation != null && endLocation != null) {
String mapUrl =
'https://www.google.com/maps/dir/${startLocation.latitude},${startLocation.longitude}/${endLocation.latitude},${endLocation.longitude}/';
showInBrowser(mapUrl);
}
},
child: const Icon(Icons.directions),
),
),
],
),
);
}
}
// A reusable widget for the main detail cards
class _DetailCard extends StatelessWidget {
final IconData icon;
final String title;
final Widget child;
const _DetailCard({
required this.icon,
required this.title,
required this.child,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.05),
margin: const EdgeInsets.only(bottom: 16.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: Theme.of(context).primaryColor),
const SizedBox(width: 8),
Text(
title,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(fontWeight: FontWeight.bold),
),
],
),
const Divider(height: 24),
child,
],
),
),
);
}
}
// A reusable widget for a label-value pair inside a card
class _InfoTile extends StatelessWidget {
final String label;
final String value;
final bool isStatus;
const _InfoTile({
required this.label,
required this.value,
this.isStatus = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: Colors.grey[700])),
if (isStatus)
_buildStatusChip(value)
else
Flexible(
child: Text(
value,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
textAlign: TextAlign.end,
),
),
],
),
);
}
}
// Reusing the status chip from the previous page for consistency
Widget _buildStatusChip(String status) {
Color chipColor;
Color textColor;
IconData iconData;
switch (status.toLowerCase()) {
case 'apply':
case 'completed': // Assuming 'Apply' means completed
chipColor = Colors.green.shade50;
textColor = Colors.green.shade800;
iconData = Icons.check_circle;
status = 'Completed';
break;
case 'refused':
chipColor = Colors.red.shade50;
textColor = Colors.red.shade800;
iconData = Icons.cancel;
break;
case 'cancel':
chipColor = Colors.orange.shade50;
textColor = Colors.orange.shade800;
iconData = Icons.info;
status = 'Cancelled';
break;
default:
chipColor = Colors.grey.shade200;
textColor = Colors.grey.shade800;
iconData = Icons.hourglass_empty;
}
return Chip(
avatar: Icon(iconData, color: textColor, size: 16),
label: Text(
status.tr,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
),
backgroundColor: chipColor,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
side: BorderSide.none,
);
}
@@ -0,0 +1,375 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/functions/upload_image.dart';
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
import 'package:sefer_driver/device_compatibility_page.dart';
import 'package:sefer_driver/main.dart';
// استيراد الصفحات الأخرى... تأكد من صحة المسارات
import 'package:sefer_driver/views/Rate/rate_app_page.dart';
import 'package:sefer_driver/views/auth/captin/contact_us_page.dart';
import 'package:sefer_driver/views/auth/captin/invite_driver_screen.dart';
import 'package:sefer_driver/views/notification/available_rides_page.dart';
import 'package:sefer_driver/views/auth/captin/logout_captain.dart';
import 'package:sefer_driver/views/home/Captin/history/history_captain.dart';
import 'package:sefer_driver/views/home/Captin/home_captain/help_captain.dart';
import 'package:sefer_driver/views/home/Captin/About Us/settings_captain.dart';
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';
// 1. إنشاء Class لتعريف بيانات كل عنصر في القائمة
class DrawerItem {
final String title;
final IconData icon;
final Color color;
final VoidCallback onTap;
DrawerItem(
{required this.title,
required this.icon,
required this.color,
required this.onTap});
}
// --- الويدجت الرئيسية للقائمة الجانبية ---
class AppDrawer extends StatelessWidget {
AppDrawer({super.key});
final ImageController imageController = Get.put(ImageController());
// 2. تعريف بيانات القائمة بشكل مركزي ومنظم
final List<DrawerItem> drawerItems = [
DrawerItem(
title: 'Balance'.tr,
icon: Icons.account_balance_wallet,
color: Colors.green,
onTap: () => Get.to(() => WalletCaptainRefactored())),
DrawerItem(
title: 'Profile'.tr,
icon: Icons.person,
color: Colors.blue,
onTap: () => Get.to(() => ProfileCaptain())),
DrawerItem(
title: 'History of Trip'.tr,
icon: Icons.history,
color: Colors.orange,
onTap: () => Get.to(() => const HistoryCaptain())),
DrawerItem(
title: 'Available for rides'.tr,
icon: Icons.drive_eta,
color: Colors.teal,
onTap: () => Get.to(() => const AvailableRidesPage())),
DrawerItem(
title: 'Notifications'.tr,
icon: Icons.notifications,
color: Colors.purple,
onTap: () => Get.to(() => const NotificationCaptain())),
DrawerItem(
title: 'Helping Center'.tr,
icon: Icons.help_center,
color: Colors.cyan,
onTap: () => Get.to(() => HelpCaptain())),
DrawerItem(
title: 'Share App'.tr,
icon: Icons.share,
color: Colors.indigo,
onTap: () => Get.to(() => InviteScreen())),
// DrawerItem(
// title: 'Maintenance Center'.tr,
// icon: Icons.build,
// color: Colors.brown,
// onTap: () => Get.to(() => MaintainCenterPage())),
// DrawerItem(
// title: 'Health Insurance'.tr,
// icon: Icons.favorite,
// color: Colors.pink,
// onTap: () => Get.to(() => AssuranceHealthPage())),
DrawerItem(
title: 'Contact Us'.tr,
icon: Icons.email,
color: Colors.blueGrey,
onTap: () => Get.to(() => ContactUsPage())),
DrawerItem(
title: 'Videos Tutorials'.tr,
icon: Icons.video_library,
color: Colors.redAccent,
onTap: () => Get.to(() => VideoListPage())),
DrawerItem(
title: 'Rate Our App'.tr,
icon: Icons.star,
color: Colors.amber,
onTap: () => Get.to(() => RatingScreen())),
DrawerItem(
title: 'Is device compatible'.tr,
icon: Icons.memory,
color: Colors.greenAccent,
onTap: () => Get.to(() => DeviceCompatibilityPage())),
DrawerItem(
title: 'Privacy Policy'.tr,
icon: Icons.memory,
color: Colors.greenAccent,
onTap: () =>
launchUrl(Uri.parse('${AppLink.server}/privacy_policy.php'))),
DrawerItem(
title: 'Settings'.tr,
icon: Icons.settings,
color: Colors.grey.shade600,
onTap: () => Get.to(() => const SettingsCaptain())),
];
@override
Widget build(BuildContext context) {
return Drawer(
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
children: [
// --- الجزء العلوي من القائمة (بيانات المستخدم) ---
UserHeader(), // استخدمنا الويدجت المحسنة بالأسفل
// --- قائمة العناصر المتحركة ---
Expanded(
child: AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: drawerItems.length + 1, // +1 لزر تسجيل الخروج
itemBuilder: (BuildContext context, int index) {
// --- زر تسجيل الخروج في النهاية ---
if (index == drawerItems.length) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _DrawerItemTile(
item: DrawerItem(
title: 'Sign Out'.tr,
icon: Icons.logout,
color: Colors.red,
onTap: () =>
Get.to(() => const LogoutCaptain())),
),
),
),
);
}
// --- بقية العناصر ---
final item = drawerItems[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _DrawerItemTile(item: item),
),
),
);
},
),
),
),
],
),
),
);
}
}
// --- ويدجت خاصة بكل عنصر في القائمة ---
class _DrawerItemTile extends StatelessWidget {
final DrawerItem item;
const _DrawerItemTile({required this.item});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: item.color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(item.icon, color: item.color, size: 24),
),
title: Text(
item.title,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w500),
),
onTap: () {
Get.back(); // لإغلاق القائمة عند الضغط
Future.delayed(const Duration(milliseconds: 250), () {
item.onTap(); // الانتقال للصفحة بعد تأخير بسيط لإظهار الأنيميشن
});
},
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
splashColor: item.color.withOpacity(0.2),
),
);
}
}
// --- ويدجت محسنة للجزء العلوي من القائمة ---
// ... (الاستيرادات السابقة تبقى كما هي)
// --- تم تعديل 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(
accountName: Text(
box.read(BoxName.nameDriver).toString(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
shadows: [Shadow(blurRadius: 2, color: Colors.black26)]),
),
accountEmail:
box.read(BoxName.emailDriver).toString().contains('intaleqapp')
? Text('Your email not updated yet'.tr)
: Text(box.read(BoxName.emailDriver)),
currentAccountPicture: GetBuilder<ImageController>(
builder: (controller) => Stack(
clipBehavior: Clip.none,
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.3), blurRadius: 5)
],
),
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(
bottom: -5,
right: -5,
child: InkWell(
onTap: () => controller.choosImagePicture(
AppLink.uploadImagePortrate, 'portrait'),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Icon(Icons.camera_alt,
color: Theme.of(context).primaryColor, size: 18),
),
),
),
],
),
),
otherAccountsPictures: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
homeCaptainController.rating.toString(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16),
),
const SizedBox(width: 4),
const Icon(Icons.star, color: Colors.amber, size: 20),
],
),
],
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Theme.of(context).primaryColor, Colors.blue.shade700],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
);
}
}
@@ -0,0 +1,189 @@
// import 'dart:async';
// import 'package:SEFER/constant/box_name.dart';
// import 'package:SEFER/main.dart';
// import 'package:SEFER/views/widgets/my_scafold.dart';
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:permission_handler/permission_handler.dart';
// import 'package:agora_rtc_engine/agora_rtc_engine.dart';
// import '../../../../constant/api_key.dart';
// import '../../../../controller/functions/crud.dart';
// String appId = AK.agoraAppId;
// class DriverCallPage extends StatefulWidget {
// const DriverCallPage({super.key});
// @override
// State<DriverCallPage> createState() => _DriverCallPageState();
// }
// class _DriverCallPageState extends State<DriverCallPage> {
// String channelName = '';
// String token = '';
// // "00612994c6e707543e68d5638894d04f989IAAlydoFEC3ZeZkeUwl0dSswZTX8n+xyZR8PBWdwXFV6t6MLiA8AAAAAEACCHD/gn3TUZQEAAQAAAAAA";
// // int uid = int.parse(box.read(BoxName.phoneDriver)); // uid of the local user
// int uid = 0;
// int? _remoteUid; // uid of the remote user
// bool _isJoined = false; // Indicates if the local user has joined the channel
// late RtcEngine agoraEngine; // Agora engine instance
// final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
// GlobalKey<ScaffoldMessengerState>(); // Global key to access the scaffold
// showMessage(String message) {
// scaffoldMessengerKey.currentState?.showSnackBar(SnackBar(
// content: Text(message),
// ));
// }
// initAgora() async {
// await fetchToken();
// await setupVoiceSDKEngine();
// }
// fetchToken() async {
// var res = await CRUD()
// .getAgoraToken(channelName: channelName, uid: uid.toString());
// setState(() {
// token = res;
// });
// }
// @override
// void initState() {
// super.initState();
// _remoteUid = box.read(BoxName.phone) != null
// ? int.parse(box.read(BoxName.phone))
// : int.parse(box.read(BoxName.phoneDriver));
// uid = box.read(BoxName.phoneDriver) != null
// ? int.parse(box.read(BoxName.phoneDriver))
// : int.parse(box.read(BoxName.phone));
// // Set up an instance of Agora engine
// initAgora();
// }
// Future<void> setupVoiceSDKEngine() async {
// // retrieve or request microphone permission
// await [Permission.microphone].request();
// //create an instance of the Agora engine
// agoraEngine = createAgoraRtcEngine();
// await agoraEngine.initialize(RtcEngineContext(appId: AK.agoraAppId));
// // Register the event handler
// agoraEngine.registerEventHandler(
// RtcEngineEventHandler(
// onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
// showMessage(
// "Local user uid:${connection.localUid} joined the channel");
// setState(() {
// _isJoined = true;
// });
// },
// onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
// showMessage("Remote user uid:$remoteUid joined the channel");
// setState(() {
// _remoteUid = remoteUid;
// });
// },
// onUserOffline: (RtcConnection connection, int remoteUid,
// UserOfflineReasonType reason) {
// showMessage("Remote user uid:$remoteUid left the channel");
// setState(() {
// _remoteUid = null;
// });
// },
// ),
// );
// }
// void join() async {
// // Set channel options including the client role and channel profile
// ChannelMediaOptions options = const ChannelMediaOptions(
// clientRoleType: ClientRoleType.clientRoleBroadcaster,
// channelProfile: ChannelProfileType.channelProfileCommunication,
// );
// await agoraEngine.joinChannel(
// token: token,
// channelId: channelName,
// options: options,
// uid: uid,
// );
// }
// //https://console.agora.io/invite?sign=5e9e22d06f22caeeada9954c9e908572%253A5ba8aed978a35eab5a5113742502ded2a41478b2a81cb19c71a30776e125b58a
// void leave() {
// setState(() {
// _isJoined = false;
// _remoteUid = null;
// });
// agoraEngine.leaveChannel();
// }
// // Clean up the resources when you leave
// @override
// void dispose() async {
// await agoraEngine.leaveChannel();
// super.dispose();
// }
// // Build UI
// @override
// Widget build(BuildContext context) {
// return MaterialApp(
// scaffoldMessengerKey: scaffoldMessengerKey,
// home: MyScafolld(
// // appBar: AppBar(
// // title: const Text('Get started with Voice Calling'),
// // ),
// title: 'Voice Calling'.tr,
// isleading: true,
// body: [
// ListView(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
// children: [
// // Status text
// Container(height: 40, child: Center(child: _status())),
// // Button Row
// Row(
// children: <Widget>[
// Expanded(
// child: ElevatedButton(
// child: const Text("Join"),
// onPressed: () => {join()},
// ),
// ),
// const SizedBox(width: 10),
// Expanded(
// child: ElevatedButton(
// child: const Text("Leave"),
// onPressed: () => {leave()},
// ),
// ),
// ],
// ),
// ],
// ),
// ]),
// );
// }
// Widget _status() {
// String statusText;
// if (!_isJoined)
// statusText = 'Join a channel';
// else if (_remoteUid == null)
// statusText = 'Waiting for a remote user to join...';
// else
// statusText = 'Connected to remote user, uid:$_remoteUid';
// return Text(
// statusText,
// );
// }
// }
@@ -0,0 +1,333 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/captin/help/help_controller.dart';
import 'package:sefer_driver/views/home/Captin/home_captain/help_details_replay_page.dart';
import 'package:flutter/cupertino.dart';
import '../../../../controller/functions/encrypt_decrypt.dart';
class HelpCaptain extends StatelessWidget {
HelpCaptain({super.key});
@override
Widget build(BuildContext context) {
Get.put(HelpController());
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(
'Helping Page'.tr,
style: const TextStyle(fontWeight: FontWeight.bold),
),
leading: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.pop(context),
child: const Icon(CupertinoIcons.back),
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(
20.0), // Increased padding around the content
child: ListView(
// crossAxisAlignment:
// CrossAxisAlignment.stretch, // Stretch children to full width
children: [
Container(
padding:
const EdgeInsets.all(18.0), // Slightly increased padding
decoration: BoxDecoration(
color:
CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.systemGrey6
: CupertinoColors.darkBackgroundGray,
borderRadius:
BorderRadius.circular(15.0), // More rounded corners
),
child: Text(
'If you need any help or have questions, this is the right place to do that. You are welcome!'
.tr,
style:
CupertinoTheme.of(context).textTheme.textStyle.copyWith(
fontSize: 16, // Slightly larger font size
color: CupertinoColors.label.resolveFrom(
context), // Ensure text color adapts to theme
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
CupertinoFormSection.insetGrouped(
// Using CupertinoFormSection for better styling
header: Text('Submit Your Question'.tr),
margin: EdgeInsets.zero,
children: [
GetBuilder<HelpController>(
builder: (helpController) => Form(
key: helpController.formKey,
child: CupertinoTextFormFieldRow(
controller: helpController.helpQuestionController,
placeholder: 'Enter your Question here'.tr,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your question'.tr;
}
return null;
},
prefix: const Icon(CupertinoIcons
.question_circle), // Added a prefix icon
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 10.0),
child: GetBuilder<HelpController>(
builder: (helpController) => helpController.isLoading
? const CupertinoActivityIndicator()
: CupertinoButton.filled(
onPressed: () {
if (helpController.formKey.currentState!
.validate()) {
helpController.addHelpQuestion();
helpController.helpQuestionController
.clear(); // Clear the text field
}
},
child: Text('Submit Question'.tr),
),
),
),
],
),
const SizedBox(height: 20),
Text(
'Your Questions'.tr,
style: CupertinoTheme.of(context)
.textTheme
.navTitleTextStyle
.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Expanded(
child: GetBuilder<HelpController>(
builder: (helpController) =>
CupertinoListSection.insetGrouped(
margin: EdgeInsets.zero,
children: helpController.helpQuestionDate['message'] != null
? List.generate(
helpController.helpQuestionDate['message'].length,
(index) {
var list = helpController
.helpQuestionDate['message'][index];
return CupertinoListTile(
title: Text(
EncryptionHelper.instance
.decryptData(list['helpQuestion']),
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
list['datecreated'],
style: CupertinoTheme.of(context)
.textTheme
.tabLabelTextStyle,
),
const Icon(CupertinoIcons.chevron_forward),
],
),
onTap: () {
helpController.getIndex(
int.parse(EncryptionHelper.instance
.decryptData(list['id'])),
EncryptionHelper.instance
.decryptData(list['helpQuestion']));
helpController
.getHelpRepley(list['id'].toString());
Get.to(() => const HelpDetailsReplayPage());
},
);
},
)
: [
Center(
child: Text('No questions asked yet.'.tr),
),
],
),
),
),
],
),
),
),
);
}
}
// class HelpCaptain extends StatelessWidget {
// HelpCaptain({super.key});
// @override
// Widget build(BuildContext context) {
// Get.put(HelpController());
// return CupertinoPageScaffold(
// navigationBar: CupertinoNavigationBar(
// middle: Text('Helping Page'.tr),
// leading: CupertinoButton(
// padding: EdgeInsets.zero,
// child: Icon(CupertinoIcons.back),
// onPressed: () => Navigator.pop(context),
// ),
// ),
// child: SafeArea(
// child: Padding(
// padding: const EdgeInsets.all(16.0),
// child: Column(
// children: [
// Padding(
// padding: const EdgeInsets.symmetric(vertical: 12.0),
// child: Container(
// padding: const EdgeInsets.all(16.0),
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(12.0),
// border: Border.all(
// color: CupertinoColors.systemGrey2,
// ),
// ),
// child: Text(
// 'If you need any help or have questions, this is the right place to do that. You are welcome!'
// .tr,
// style: CupertinoTheme.of(context).textTheme.textStyle,
// textAlign: TextAlign.center,
// ),
// ),
// ),
// Card(
// elevation: 3,
// color: CupertinoColors.systemGrey6,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: Padding(
// padding: const EdgeInsets.all(16.0),
// child: GetBuilder<HelpController>(
// builder: (helpController) => Form(
// key: helpController.formKey,
// child: Column(
// children: [
// CupertinoTextField(
// controller: helpController.helpQuestionController,
// placeholder: 'Enter your Question here'.tr,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(12),
// border: Border.all(
// color: CupertinoColors.systemGrey,
// ),
// ),
// padding: const EdgeInsets.all(16),
// clearButtonMode: OverlayVisibilityMode.editing,
// ),
// const SizedBox(height: 20),
// helpController.isLoading
// ? const CupertinoActivityIndicator()
// : CupertinoButton.filled(
// onPressed: () {
// if (helpController.formKey.currentState!
// .validate()) {
// helpController.addHelpQuestion();
// // Clear the feedback form
// helpController.formKey.currentState!
// .reset();
// }
// },
// child: Text('Submit Question'.tr),
// ),
// ],
// ),
// ),
// ),
// ),
// ),
// const SizedBox(height: 20),
// Expanded(
// child: GetBuilder<HelpController>(
// builder: (helpController) => Padding(
// padding: const EdgeInsets.all(10),
// child: Container(
// decoration: BoxDecoration(
// border: Border.all(
// color: CupertinoColors.systemGrey2,
// ),
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: ListView.builder(
// itemCount: helpController.helpQuestionDate['message'] !=
// null
// ? helpController.helpQuestionDate['message'].length
// : 0,
// itemBuilder: (BuildContext context, int index) {
// var list =
// helpController.helpQuestionDate['message'][index];
// return Padding(
// padding: const EdgeInsets.all(3),
// child: Container(
// padding: const EdgeInsets.symmetric(
// vertical: 12, horizontal: 16),
// decoration: BoxDecoration(
// border: Border.all(
// color: CupertinoColors.activeGreen,
// width: 2,
// ),
// borderRadius: BorderRadius.circular(12),
// ),
// child: GestureDetector(
// onTap: () {
// helpController.getIndex(
// list['id'], list['helpQuestion']);
// helpController
// .getHelpRepley(list['id'].toString());
// Get.to(() => const HelpDetailsReplayPage());
// },
// child: Row(
// mainAxisAlignment:
// MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: Text(
// list['helpQuestion'],
// style: CupertinoTheme.of(context)
// .textTheme
// .textStyle,
// overflow: TextOverflow.ellipsis,
// ),
// ),
// Text(
// list['datecreated'],
// style: CupertinoTheme.of(context)
// .textTheme
// .tabLabelTextStyle,
// ),
// ],
// ),
// ),
// ),
// );
// },
// ),
// ),
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
// }
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../../controller/functions/encrypt_decrypt.dart';
import '../../../../controller/home/captin/help/help_controller.dart';
import '../../../widgets/my_scafold.dart';
class HelpDetailsReplayPage extends StatelessWidget {
const HelpDetailsReplayPage({super.key});
@override
Widget build(BuildContext context) {
Get.find<HelpController>();
return GetBuilder<HelpController>(
builder: (helpController) => MyScafolld(
title: 'Help Details'.tr,
body: [
helpController.isLoading
? const MyCircularProgressIndicator()
: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Card(
elevation: 3,
child: Container(
width: Get.width * .66,
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
helpController.qustion,
style: AppStyle.title,
),
),
),
),
]),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Card(
elevation: 3,
child: Container(
color: Colors.transparent,
width: Get.width * .66,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: helpController.status ==
'not yet' ||
EncryptionHelper.instance
.decryptData(helpController
.helpQuestionRepleyDate[
'message']['replay'])
.toString() ==
'not yet'
? Text(
'No Response yet.'.tr,
style: AppStyle.title,
)
: Text(
EncryptionHelper.instance
.decryptData(helpController
.helpQuestionRepleyDate[
'message']['replay'])
.toString(),
style: AppStyle.title,
),
),
),
),
]),
],
)
],
isleading: true,
));
}
}
@@ -0,0 +1,763 @@
import 'dart:io';
import 'dart:ui';
import 'package:bubble_head/bubble.dart';
// import 'package:google_maps_flutter/google_maps_flutter.dart';
// import 'package:flutter_map/flutter_map.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// import 'package:latlong2/latlong.dart' as latlng; // لإحداثيات العرض
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:sefer_driver/views/home/Captin/home_captain/drawer_captain.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
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';
import '../../../../controller/home/captin/home_captain_controller.dart';
import '../../../../controller/home/captin/map_driver_controller.dart';
import '../../../../main.dart';
import '../../../notification/available_rides_page.dart';
import '../../../widgets/circle_container.dart';
import '../driver_map_page.dart';
import 'widget/connect.dart';
import 'widget/left_menu_map_captain.dart';
// ==================================================================
// Redesigned Main Widget (V3)
// ==================================================================
class HomeCaptain extends StatelessWidget {
HomeCaptain({super.key});
final LocationController locationController =
Get.put(LocationController(), permanent: true);
final HomeCaptainController homeCaptainController =
Get.put(HomeCaptainController());
@override
Widget build(BuildContext context) {
// Initial calls remain the same.
// Get.put(HomeCaptainController());
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: 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(),
],
),
),
);
}
}
// ==================================================================
// Redesigned Helper Widgets (V3)
// ==================================================================
/// 1. The AppBar now contains the map actions in a PopupMenuButton.
class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
const _HomeAppBar();
@override
Widget build(BuildContext context) {
final homeCaptainController = Get.find<HomeCaptainController>();
return AppBar(
backgroundColor: Colors.white,
elevation: 1,
shadowColor: Colors.black.withOpacity(0.1),
title: Row(
children: [
Image.asset(
'assets/images/logo.gif',
height: 35,
),
const SizedBox(width: 10),
Text(
AppInformation.appName.split(' ')[0].toString().tr,
style: AppStyle.title.copyWith(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColor.blueColor,
),
),
],
),
actions: [
// Refuse count indicator
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Center(
child: MyCircleContainer(
child: Text(
homeCaptainController.countRefuse.toString(),
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
),
),
),
),
// The new PopupMenuButton for all map and ride actions.
Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 4,
),
],
),
child: Row(
children: [
_MapControlButton(
iconColor: Colors.blue,
icon: Icons.satellite_alt,
tooltip: 'Change Map Type'.tr,
onPressed: homeCaptainController.changeMapType,
),
// _MapControlButton(
// iconColor: Colors.blue,
// icon: Icons.streetview_sharp,
// tooltip: 'Toggle Traffic'.tr,
// onPressed: homeCaptainController.changeMapTraffic,
// ),
GetBuilder<HomeCaptainController>(
builder: (controller) {
return _MapControlButton(
// تغيير الأيقونة واللون بناءً على الحالة
icon: Icons.local_fire_department,
iconColor: controller.isHeatmapVisible
? Colors.orange
: Colors.grey,
tooltip: 'Show Heatmap'.tr,
onPressed: controller.toggleHeatmap,
);
},
),
_MapControlButton(
iconColor: Colors.blue,
icon: Icons.my_location, // Changed for clarity
tooltip: 'Center on Me'.tr,
onPressed: () {
if (homeCaptainController.mapHomeCaptainController != null) {
homeCaptainController.mapHomeCaptainController!
.animateCamera(CameraUpdate.newLatLngZoom(
Get.find<LocationController>().myLocation,
17.5,
));
}
},
),
],
),
),
],
);
}
PopupMenuItem<String> _buildPopupMenuItem({
required String value,
IconData? icon,
Widget? iconWidget,
required String text,
Color? iconColor,
}) {
return PopupMenuItem<String>(
value: value,
child: Row(
children: [
iconWidget ?? Icon(icon, color: iconColor ?? Colors.grey.shade600),
const SizedBox(width: 16),
Text(text),
],
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
/// 2. The Map View is unchanged functionally.
class _MapView extends StatelessWidget {
const _MapView();
@override
Widget build(BuildContext context) {
// جلب الكونترولر الرئيسي
final homeController = Get.find<HomeCaptainController>();
return GetBuilder<HomeCaptainController>(
builder: (controller) {
if (controller.isLoading) {
return const MyCircularProgressIndicator();
}
// --- هذا هو التعديل ---
// هذا الـ Builder يستمع إلى تحديثات الموقع
return GetBuilder<LocationController>(
builder: (locationController) {
// if (homeController.mapHomeCaptainController != null &&
// homeController.isActive) {
// homeController.mapHomeCaptainController!.animateCamera(
// CameraUpdate.newCameraPosition(
// CameraPosition(
// target: locationController.myLocation, // الموقع الجديد
// zoom: 17.5, // تقريب لمتابعة السائق
// tilt: 50.0, // زاوية رؤية 3D
// bearing: locationController.heading, // اتجاه السيارة
// ),
// ),
// );
// }
// --- نهاية الكود الجديد ---
// إرجاع الخريطة
return GoogleMap(
padding: const EdgeInsets.only(bottom: 110, top: 300),
fortyFiveDegreeImageryEnabled: true,
onMapCreated: controller.onMapCreated,
// onCameraMove: controller.onCameraMove,
minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
initialCameraPosition: CameraPosition(
target: locationController.myLocation,
zoom: 15,
),
// --- تم حذف onCameraMove الخاطئ ---
// === إضافة الطبقة الحرارية هنا ===
polygons: controller.heatmapPolygons,
// =
markers: {
Marker(
markerId: MarkerId('MyLocation'.tr),
position: locationController.myLocation, // يتم تحديثه من هنا
rotation: locationController.heading, // يتم تحديثه من هنا
flat: true,
anchor: const Offset(0.5, 0.5),
icon: controller.carIcon,
)
},
mapType: controller.mapType ? MapType.satellite : MapType.terrain,
myLocationButtonEnabled: false,
myLocationEnabled: false,
trafficEnabled: controller.mapTrafficON,
buildingsEnabled: false,
mapToolbarEnabled: false,
compassEnabled: false,
zoomControlsEnabled: false,
);
// --- الكود الجديد ---
// // تحويل الإحداثيات من جوجل (إذا كنت لا تزال تستخدمها) إلى latlong2
// final latlng.LatLng currentCarPosition = latlng.LatLng(
// locationController.myLocation.latitude,
// locationController.myLocation.longitude);
// return FlutterMap(
// // 1. تمرير الـ Controller الذي أنشأناه في الخطوة 2
// mapController: homeController.mapController,
// options: MapOptions(
// // 2. هذا بديل initialCameraPosition
// initialCenter: currentCarPosition,
// initialZoom: 15,
// // هذا بديل padding
// // (ملاحظة: flutter_map لا يدعم padding مباشرة، قد تحتاج لتعديل الواجهة
// // أو استخدام خاصية nonRotatedChildren لبدائل أخرى)
// // هذا بديل minMaxZoomPreference
// minZoom: 12,
// maxZoom: 16, onMapReady: homeController.onMapReady,
// ),
// // 3. الخرائط في flutter_map عبارة عن "طبقات" (Layers)
// children: [
// // --- الطبقة الأولى: الخريطة الأساسية (OSM) ---
// TileLayer(
// urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
// userAgentPackageName:
// 'com.your.app.name', // هام: ضع اسم تطبيقك هنا
// ),
// // --- (اختياري) طبقة القمر الصناعي بناءً على MapType ---
// if (controller.mapType) // إذا كنت لا تزال تستخدم mapType
// // TileLayer(
// // // ملاحظة: هذا الرابط يحتاج مفتاح API من MapTiler أو مزود آخر
// // urlTemplate:
// // 'https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=YOUR_API_KEY',
// // userAgentPackageName: 'com.your.app.name',
// // ),
// // --- الطبقة الثانية: أيقونة السيارة (Marker) ---
// MarkerLayer(
// markers: [
// Marker(
// point: currentCarPosition, // الإحداثيات
// width: 80,
// height: 80,
// child: Transform.rotate(
// // 4. هذا بديل rotation
// angle: locationController.heading *
// (3.1415926535 / 180), // تحويل من درجات إلى راديان
// // 5. هذا بديل carIcon (أصبح أسهل!)
// child: Image.asset(
// 'assets/images/car.png', // نفس المسار الذي استخدمته من قبل
// width: 30, // الحجم الذي حددته في addCustomCarIcon
// height: 35,
// ),
// ),
// ),
// ],
// ),
// ],
// );
},
);
},
);
}
}
/// 3. The floating "Status Pod" at the bottom of the screen.
class _StatusPodOverlay extends StatelessWidget {
const _StatusPodOverlay();
void _showDetailsDialog(
BuildContext context, HomeCaptainController controller) {
Get.dialog(
_DriverDetailsDialog(controller), // تمرير الكنترولر هنا
barrierColor: Colors.black.withOpacity(0.3),
);
}
@override
Widget build(BuildContext context) {
final homeCaptainController = Get.find<HomeCaptainController>();
return Positioned(
bottom: 16,
left: 16,
right: 16,
child: GestureDetector(
onTap: () => _showDetailsDialog(context, homeCaptainController),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.85),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withOpacity(0.5)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
spreadRadius: -5,
)
],
),
child: Row(
children: [
const ConnectWidget(),
const Spacer(),
_buildQuickStat(
icon: Icons.directions_car_rounded,
value: homeCaptainController.countRideToday,
label: 'Rides'.tr,
color: AppColor.blueColor,
),
const SizedBox(width: 16),
_buildQuickStat(
icon: Entypo.wallet,
value: homeCaptainController.totalMoneyToday.toString(),
label: 'Today'.tr,
color: AppColor.greenColor,
),
const SizedBox(width: 8),
],
),
),
),
),
),
);
}
Widget _buildQuickStat(
{required IconData icon,
required String value,
required String label,
required Color color}) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 4),
Text(value,
style: AppStyle.title
.copyWith(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
Text(label,
style: AppStyle.title
.copyWith(fontSize: 12, color: Colors.grey.shade700)),
],
);
}
}
/// 4. The Dialog that shows detailed driver stats.
class _DriverDetailsDialog extends StatelessWidget {
// 1. إضافة متغير للكنترولر
final HomeCaptainController controller;
// 2. تحديث البناء لاستقباله
const _DriverDetailsDialog(this.controller);
@override
Widget build(BuildContext context) {
// 3. حذف السطر الذي يسبب الخطأ: final homeCaptainController = Get.find...
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: AlertDialog(
backgroundColor: Colors.white.withOpacity(0.95),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
titlePadding: const EdgeInsets.only(top: 20),
title: Center(
child: Text(
'Your Activity'.tr,
style: AppStyle.title
.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 20),
_buildStatRow(
icon: Entypo.wallet,
color: AppColor.greenColor,
label: 'Today'.tr,
// استخدام المتغير controller الذي تم تمريره
value: controller.totalMoneyToday.toString(),
),
const SizedBox(height: 12),
_buildStatRow(
icon: Entypo.wallet,
color: AppColor.yellowColor,
label: AppInformation.appName,
value: controller.totalMoneyInSEFER.toString(),
),
const Divider(height: 24),
_buildDurationRow(
icon: Icons.timer_outlined,
label: 'Active Duration:'.tr,
value: controller.stringActiveDuration,
color: AppColor.greenColor,
),
const SizedBox(height: 12),
_buildDurationRow(
icon: Icons.access_time,
label: 'Total Connection Duration:'.tr,
value: controller.totalDurationToday,
color: AppColor.accentColor,
),
const Divider(height: 24),
_buildStatRow(
icon: Icons.star_border_rounded,
color: AppColor.blueColor,
label: 'Total Points'.tr,
value: controller.totalPoints.toString(),
),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('Close'.tr,
style: AppStyle.title.copyWith(
color: AppColor.blueColor, fontWeight: FontWeight.bold)),
)
],
),
);
}
// ... بقية الدوال المساعدة (_buildStatRow, _buildDurationRow) تبقى كما هي ...
Widget _buildStatRow(
{required IconData icon,
required Color color,
required String label,
required String value}) {
return Row(
children: [
Icon(icon, color: color, size: 22),
const SizedBox(width: 12),
Text('$label:', style: AppStyle.title),
const Spacer(),
Text(
value,
style: AppStyle.title.copyWith(
color: color, fontWeight: FontWeight.bold, fontSize: 18),
),
],
);
}
Widget _buildDurationRow(
{required IconData icon,
required String label,
required String value,
required Color color}) {
return Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Text(label, style: AppStyle.title),
const Spacer(),
Text(
value,
style: AppStyle.title.copyWith(
fontWeight: FontWeight.bold, color: color, fontSize: 16),
),
],
);
}
}
class _MapControlButton extends StatelessWidget {
final IconData icon;
final VoidCallback onPressed;
final String tooltip;
const _MapControlButton({
required this.icon,
required this.onPressed,
required this.tooltip,
required MaterialColor iconColor,
});
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onPressed,
child: Container(
padding: const EdgeInsets.all(8),
child: Icon(
icon,
size: 24,
color: AppColor.blueColor,
),
),
),
),
);
}
}
class FloatingActionButtons extends StatelessWidget {
const FloatingActionButtons();
@override
Widget build(BuildContext context) {
// نفس الكود الأصلي للأزرار
return Positioned(
bottom: Get.height * .2,
right: 6,
child:
GetBuilder<HomeCaptainController>(builder: (homeCaptainController) {
return Column(
children: [
Platform.isAndroid
? AnimatedContainer(
duration: const Duration(microseconds: 200),
width: homeCaptainController.widthMapTypeAndTraffic,
decoration: BoxDecoration(
border: Border.all(color: AppColor.blueColor),
color: AppColor.secondaryColor,
borderRadius: BorderRadius.circular(15)),
child: IconButton(
onPressed: () async {
Bubble().startBubbleHead(sendAppToBackground: true);
},
icon: Image.asset(
'assets/images/logo1.png',
fit: BoxFit.cover,
width: 35,
height: 35,
),
),
)
: const SizedBox(),
const SizedBox(
height: 5,
),
AnimatedContainer(
duration: const Duration(microseconds: 200),
width: homeCaptainController.widthMapTypeAndTraffic,
decoration: BoxDecoration(
border: Border.all(color: AppColor.blueColor),
color: AppColor.secondaryColor,
borderRadius: BorderRadius.circular(15)),
child: IconButton(
onPressed: () {
Get.to(() => const AvailableRidesPage());
},
icon: const Icon(
Icons.train_sharp,
size: 29,
color: AppColor.blueColor,
),
),
),
const SizedBox(
height: 5,
),
// هذا الكود يوضع داخل الـ Stack في ملف الواجهة (HomeCaptain View)
box.read(BoxName.rideStatus) == 'Applied' ||
box.read(BoxName.rideStatus) == 'Begin'
? Positioned(
bottom: Get.height * .2,
// جعلنا الزر يظهر في المنتصف أو يمتد ليكون واضحاً جداً
right: 20,
left: 20,
child: Center(
child: AnimatedContainer(
duration: const Duration(
milliseconds:
200), // تم تصحيح microseconds إلى milliseconds لحركة أنعم
// أزلنا العرض الثابت homeCaptainController.widthMapTypeAndTraffic لكي يتسع للنص
// width: homeCaptainController.widthMapTypeAndTraffic,
decoration: BoxDecoration(
border: Border.all(
color: AppColor.blueColor,
width: 2), // تعريض الإطار قليلاً
color: AppColor.secondaryColor, // لون الخلفية
borderRadius: BorderRadius.circular(
30), // تدوير الحواف ليشبه الأزرار الحديثة
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
)
]),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(30),
onLongPress: () {
// وظيفة الحذف عند الضغط الطويل (للطوارئ)
box.write(BoxName.rideStatus, 'delete');
homeCaptainController.update();
},
onTap: () {
// نفس منطقك الأصلي للانتقال
if (box.read(BoxName.rideStatus) == 'Applied') {
Get.to(() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArguments));
Get.put(MapDriverController())
.changeRideToBeginToPassenger();
} else {
Get.to(() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArguments));
Get.put(MapDriverController())
.startRideFromStartApp();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
child: Row(
mainAxisSize:
MainAxisSize.min, // حجم الزر على قد المحتوى
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons
.directions_car_filled_rounded, // تغيير الأيقونة لسيارة أو اتجاهات لتكون معبرة أكثر
size: 24,
color: AppColor.blueColor,
),
const SizedBox(
width: 10), // مسافة بين الأيقونة والنص
Text(
"متابعة الرحلة", // النص الواضح للسائق
style: const TextStyle(
color: AppColor.blueColor,
fontSize: 16,
fontWeight: FontWeight.bold,
fontFamily:
'Cairo', // تأكد من نوع الخط المستخدم عندك
),
),
if (box.read(BoxName.rideStatus) ==
'Begin') ...[
const SizedBox(width: 5),
// إضافة مؤشر صغير (نقطة حمراء) إذا كانت الرحلة قد بدأت بالفعل (اختياري)
const Icon(Icons.circle,
size: 8, color: Colors.green)
]
],
),
),
),
),
),
),
)
: const SizedBox()
],
);
}),
);
}
}
Future<void> checkForAppliedRide(BuildContext context) async {
checkForPendingOrderFromServer();
}
@@ -0,0 +1,84 @@
// import 'package:flutter/material.dart';
// import 'package:flutter_map/flutter_map.dart';
// import 'package:get/get.dart';
// import 'package:google_maps_flutter/google_maps_flutter.dart';
// import 'package:latlong2/latlong.dart';
// import '../../../../controller/functions/location_controller.dart';
// import '../../../../controller/home/captin/home_captain_controller.dart';
// // هذه ويدجت بديلة للـ _MapView في الكود الخاص بك
// // V3 - MapView Replacement using flutter_map
// class OsmMapView extends StatelessWidget {
// const OsmMapView({super.key});
// @override
// Widget build(BuildContext context) {
// // افترض أنك تحصل على الموقع والاتجاه بنفس الطريقة من الكونترولر
// final LocationController locationController =
// Get.find<LocationController>();
// final HomeCaptainController homeCaptainController =
// Get.find<HomeCaptainController>();
// // يمكنك استخدام GetBuilder لمراقبة التغييرات في الموقع
// return Obx(() {
// final LatLng currentLocation = LatLng(
// locationController.myLocation.latitude,
// locationController.myLocation.longitude);
// final double currentHeading = locationController.heading;
// return FlutterMap(
// // يمكنك ربط هذا بـ MapController الخاص بـ flutter_map
// // mapController: homeCaptainController.flutterMapController,
// options: MapOptions(
// initialCenter: currentLocation,
// initialZoom: 15,
// maxZoom: 18,
// minZoom: 6,
// // تدوير الخريطة (اختياري)
// initialRotation: currentHeading,
// ),
// children: [
// // 1. طبقة الخريطة الأساسية (Tiles)
// // هذا هو الرابط لخرائط OSM الأساسية
// TileLayer(
// urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
// subdomains: ['a', 'b', 'c'],
// userAgentPackageName:
// 'com.example.app', // استبدل باسم الباكج الخاص بك
// // لاستخدام الخرائط الأوفلاين (بعد إعداد flutter_map_tile_caching)
// // tileProvider: CachedTileProvider(),
// ),
// // 2. طبقة العلامات (Markers)
// MarkerLayer(
// markers: [
// Marker(
// width: 80.0,
// height: 80.0,
// point: currentLocation,
// child: Transform.rotate(
// angle: currentHeading *
// (3.1415926535 / 180), // تحويل من درجات إلى راديان
// child: Image.asset(
// 'assets/images/car_icon.png', // تأكد أن لديك أيقونة السيارة
// // يمكنك استخدام نفس الـ carIcon من الكونترولر
// // icon: homeCaptainController.carIcon, (ملاحظة: flutter_map تستخدم ويدجت)
// ),
// ),
// ),
// ],
// ),
// // يمكنك إضافة طبقات أخرى هنا (مثل الخطوط Polylines أو المضلعات Polygons)
// ],
// );
// });
// }
// }
// // ملاحظة: ستحتاج إلى تعديل بسيط في HomeCaptainController
// // لإنشاء MapController الخاص بـ flutter_map بدلاً من GoogleMapController
// // import 'package:flutter_map/flutter_map.dart';
// // MapController flutterMapController = MapController();
@@ -0,0 +1,84 @@
// import 'package:SEFER/constant/colors.dart';
// import 'package:SEFER/constant/style.dart';
// import 'package:SEFER/controller/firebase/firbase_messge.dart';
// import 'package:SEFER/controller/home/captin/map_driver_controller.dart';
// import 'package:SEFER/views/widgets/my_scafold.dart';
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:SEFER/controller/home/captin/home_captain_controller.dart';
// import '../../../../../controller/functions/call_controller.dart';
// class CallPage extends StatelessWidget {
// const CallPage({super.key});
// @override
// Widget build(BuildContext context) {
// return MyScafolld(
// title: 'Call Page'.tr, isleading: true, body: [callPage()]);
// }
// }
// GetBuilder<HomeCaptainController> callPage() {
// CallController callController = Get.put(CallController());
// Get.put(MapDriverController());
// // callController.join();
// return GetBuilder<HomeCaptainController>(
// builder: (controller) => Positioned(
// top: Get.height * .2,
// child: Container(
// height: 100, width: Get.width,
// decoration: AppStyle.boxDecoration,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// children: [
// GestureDetector(
// onTap: () async {
// callController.join();
// },
// child: Container(
// width: 50,
// height: 50,
// decoration: const BoxDecoration(
// shape: BoxShape.circle, color: AppColor.greenColor),
// child: const Icon(
// Icons.phone,
// size: 35,
// color: AppColor.secondaryColor,
// )),
// ),
// Column(
// children: [
// Text(callController.status),
// Text(Get.find<MapDriverController>().passengerName.toString()),
// ],
// ),
// GestureDetector(
// onTap: () async {
// FirebaseMessagesController().sendNotificationToPassengerToken(
// 'Call End'.tr,
// 'Call End',
// Get.find<MapDriverController>().tokenPassenger,
// [],
// 'iphone_ringtone.wav');
// callController.leave();
// Get.back();
// },
// child: Container(
// width: 50,
// height: 50,
// decoration: const BoxDecoration(
// shape: BoxShape.circle, color: AppColor.redColor),
// child: const Icon(
// Icons.phone_disabled_sharp,
// size: 35,
// color: AppColor.secondaryColor,
// )),
// )
// ],
// ),
// // ignore: prefer_const_constructors
// ),
// ),
// );
// }
@@ -0,0 +1,141 @@
import 'package:sefer_driver/controller/functions/tts.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import '../../../../../constant/style.dart';
import '../../../../../controller/functions/encrypt_decrypt.dart';
import '../../../../widgets/elevated_btn.dart';
import '../../../../../controller/home/captin/home_captain_controller.dart';
class ConnectWidget extends StatelessWidget {
const ConnectWidget({
super.key,
});
@override
Widget build(BuildContext context) {
// final OrderRequestController orderRequestController =
// Get.put(OrderRequestController());
CaptainWalletController captainWalletController =
Get.put(CaptainWalletController());
int refusedRidesToday = 0;
// captainWalletController.getCaptainWalletFromBuyPoints();
return Center(
child: GetBuilder<HomeCaptainController>(
builder: (homeCaptainController) => double.parse(
(captainWalletController.totalPoints)) <
-200
? CupertinoButton(
onPressed: () {
Get.defaultDialog(
// backgroundColor: CupertinoColors.destructiveRed,
barrierDismissible: false,
title: double.parse(
(captainWalletController.totalPoints)) <
-200
? 'You dont have Points'.tr
: 'You Are Stopped For this Day !'.tr,
titleStyle: AppStyle.title,
content: Column(
children: [
IconButton(
onPressed: () async {
double.parse((captainWalletController
.totalPoints)) <
-200
? await Get.find<TextToSpeechController>()
.speakText(
'You must be recharge your Account'
.tr)
: await Get.find<TextToSpeechController>()
.speakText(
'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
.tr);
},
icon: const Icon(Icons.headphones),
),
Text(
double.parse((captainWalletController
.totalPoints)) <
-200
? 'You must be recharge your Account'.tr
: 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
.tr,
style: AppStyle.title,
),
],
),
confirm: double.parse(
(captainWalletController.totalPoints)) <
-200
? MyElevatedButton(
title: 'Recharge my Account'.tr,
onPressed: () {
homeCaptainController.goToWalletFromConnect();
})
: MyElevatedButton(
title: 'Ok , See you Tomorrow'.tr,
onPressed: () {
Get.back();
Get.back();
}));
},
color: CupertinoColors.destructiveRed,
child: Text(
'You are Stopped'.tr,
style: AppStyle.title,
),
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: homeCaptainController.isActive
? [Colors.green.shade400, Colors.green.shade700]
: [Colors.grey.shade400, Colors.grey.shade700],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: homeCaptainController.isActive
? Colors.green.withOpacity(0.3)
: Colors.grey.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: CupertinoButton(
onPressed: homeCaptainController.onButtonSelected,
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
homeCaptainController.isActive
? CupertinoIcons.check_mark_circled_solid
: CupertinoIcons.circle,
color: Colors.white,
size: 24,
),
const SizedBox(width: 8),
Text(
homeCaptainController.isActive
? 'Connected'.tr
: 'Not Connected'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
)),
);
}
}
@@ -0,0 +1,342 @@
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/firebase/local_notification.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/auth/captin/otp_page.dart';
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import '../../../../../constant/colors.dart';
import '../../../../../constant/links.dart';
import '../../../../../controller/firebase/firbase_messge.dart';
import '../../../../../controller/firebase/notification_service.dart';
import '../../../../../controller/functions/crud.dart';
import '../../../../../controller/home/captin/order_request_controller.dart';
import '../../../../../controller/home/navigation/navigation_view.dart';
import '../../../../../print.dart';
import '../../../../Rate/ride_calculate_driver.dart';
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
final firebaseMessagesController =
Get.isRegistered<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: Get.put(FirebaseMessagesController());
return GetBuilder<HomeCaptainController>(
builder: (controller) => Positioned(
bottom: Get.height * .2,
left: 6,
child: Column(
children: [
AnimatedContainer(
duration: const Duration(microseconds: 200),
width: controller.widthMapTypeAndTraffic,
decoration: BoxDecoration(
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.blueColor),
borderRadius: BorderRadius.circular(15)),
child: Builder(builder: (context) {
return IconButton(
onPressed: () async {
await checkForPendingOrderFromServer();
Log.print(
'box.read(BoxName.rideArgumentsFromBackground): ${box.read(BoxName.rideArgumentsFromBackground)}');
box.read(BoxName.rideArgumentsFromBackground).toString() !=
'failure'
? Get.to(() => PassengerLocationMapPage(),
arguments:
box.read(BoxName.rideArgumentsFromBackground))
: MyDialog().getDialog(
'Ride info'.tr,
'you dont have accepted ride'.tr,
() {
Get.back();
},
);
// 'box.read(BoxName.rideArgumentsFromBackground): ${box.read(BoxName.rideArgumentsFromBackground)}');
},
icon: Icon(
Icons.directions_car_rounded,
size: 29,
color:
box.read(BoxName.rideArgumentsFromBackground) == 'failure'
? AppColor.redColor
: AppColor.greenColor,
),
);
}),
),
const SizedBox(
height: 5,
),
AnimatedContainer(
duration: const Duration(microseconds: 200),
width: controller.widthMapTypeAndTraffic,
decoration: BoxDecoration(
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.blueColor),
borderRadius: BorderRadius.circular(15)),
child: IconButton(
onLongPress: () {
box.write(BoxName.statusDriverLocation, 'off');
},
onPressed: () {
// NotificationController1()
// .showNotification('Sefer Driver'.tr, ''.tr, '', '');
final now = DateTime.now();
DateTime? lastRequestTime =
box.read(BoxName.lastTimeStaticThrottle);
if (lastRequestTime == null ||
now.difference(lastRequestTime).inMinutes >= 2) {
// Update the last request time to now
lastRequestTime = now;
box.write(BoxName.lastTimeStaticThrottle, lastRequestTime);
// Navigate to the RideCalculateDriver page
Get.to(() => RideCalculateDriver());
} else {
// Optionally show a message or handle the throttling case
final minutesLeft =
2 - now.difference(lastRequestTime).inMinutes;
// Get.snackbar(
// '${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
// '');
NotificationController().showNotification(
'Intaleq Driver'.tr,
'${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
'ding',
'');
}
},
icon: const Icon(
FontAwesome5.chart_bar,
size: 29,
color: AppColor.blueColor,
),
),
),
const SizedBox(
height: 5,
),
// Platform.isAndroid
// ?
int.parse(box.read(BoxName.carYear).toString()) > 2023
? AnimatedContainer(
duration: const Duration(microseconds: 200),
width: controller.widthMapTypeAndTraffic,
decoration: BoxDecoration(
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.blueColor),
borderRadius: BorderRadius.circular(15)),
child: Builder(builder: (context) {
return IconButton(
onPressed: () async {
// mySnakeBarError('ad');
Get.to(() => const VipOrderPage());
},
icon: const Icon(
Octicons.watch,
size: 29,
color: AppColor.blueColor,
),
);
}),
)
: const SizedBox(),
// const SizedBox(
// height: 5,
// ),
// AnimatedContainer(
// duration: const Duration(microseconds: 200),
// width: controller.widthMapTypeAndTraffic,
// decoration: BoxDecoration(
// color: AppColor.secondaryColor,
// border: Border.all(color: AppColor.blueColor),
// borderRadius: BorderRadius.circular(15)),
// child: Builder(builder: (context) {
// return IconButton(
// onPressed: () async {
// box.remove(BoxName.agreeTerms);
// Get.to(() => const NavigationView());
// // box.write(BoxName.statusDriverLocation, 'off');
// },
// icon: const Icon(
// FontAwesome5.map,
// size: 29,
// color: AppColor.blueColor,
// ),
// );
// }),
// ),
// AnimatedContainer(
// duration: const Duration(microseconds: 200),
// width: controller.widthMapTypeAndTraffic,
// decoration: BoxDecoration(
// color: AppColor.secondaryColor,
// border: Border.all(color: AppColor.blueColor),
// borderRadius: BorderRadius.circular(15)),
// child: Builder(builder: (context) {
// return IconButton(
// onPressed: () async {
// NotificationService.sendNotification(
// target: 'service', // الإرسال لجميع المشتركين في "service"
// title: 'طلب خدمة جديد',
// body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.',
// isTopic: true,
// category: 'new_service_request', // فئة توضح نوع الإشعار
// );
// },
// icon: const Icon(
// FontAwesome5.grin_tears,
// size: 29,
// color: AppColor.blueColor,
// ),
// );
// }),
// ),
const SizedBox(
height: 5,
),
],
),
),
);
}
Future<void> checkForPendingOrderFromServer() async {
bool _isProcessingOrder = false;
if (_isProcessingOrder) return;
final driverId = box.read(BoxName.driverID)?.toString();
if (driverId == null) return; // Can't check without a driver ID
_isProcessingOrder = true; // Lock
try {
// You need to create this CRUD method
var response = await CRUD().post(
link: AppLink.getArgumentAfterAppliedFromBackground,
payload: {'driver_id': driverId},
);
Log.print('response getArgumentAfterAppliedFromBackground: ${response}');
// Assuming the server returns order data if found, or 'failure'/'none' if not
if (response != 'failure') {
final Map<String, dynamic> orderInfoFromServer = response['message'];
final Map<String, dynamic> rideArguments =
_transformServerDataToAppArguments(orderInfoFromServer);
// 2. Build the new arguments map, matching your Flutter structure
final customerToken = (response)['message']['token_passenger'];
final orderId = (response)['message']['ride_id'].toString();
box.write(BoxName.rideArgumentsFromBackground, rideArguments);
box.write(BoxName.statusDriverLocation, 'on');
box.write(BoxName.rideStatus, 'Apply');
Get.put(OrderRequestController()).changeApplied();
// MyDialog().getDialog(orderId.toString(), customerToken, () {});
// Now proceed with the UI flow
// _sendAcceptanceNotification(customerToken, orderId.toString());
// await _bringAppToForegroundAndNavigate(orderId);
Get.to(() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArgumentsFromBackground));
} else {
box.write(BoxName.rideArgumentsFromBackground, 'failure');
}
} catch (e) {
} finally {
_isProcessingOrder = false; // Release lock
}
}
Map<String, dynamic> _transformServerDataToAppArguments(
Map<String, dynamic> serverData) {
// Helper function to safely get and convert values to String
String _getString(String key, [String defaultValue = 'unknown']) {
// serverData[key] might be an int, double, or string. .toString() handles all.
// If it's null, use the default value.
return serverData[key]?.toString() ?? defaultValue;
}
return {
'passengerLocation': _getString('passenger_location'),
'passengerDestination': _getString('passenger_destination'),
'Duration': _getString('duration'),
'totalCost': _getString('total_cost'),
'Distance': _getString('distance'),
'name': _getString('name'),
'phone': _getString('phone'),
'email': _getString('email'),
'tokenPassenger': _getString('token_passenger'),
'direction': _getString('direction_url'),
'DurationToPassenger': _getString('duration_to_passenger'),
'rideId': _getString('ride_id'),
'passengerId': _getString('passenger_id'),
'driverId': _getString('driver_id'),
'durationOfRideValue': _getString('duration_of_ride'),
'paymentAmount': _getString('payment_amount'),
'paymentMethod': _getString('payment_method'),
'passengerWalletBurc': _getString('passenger_wallet_burc'),
'timeOfOrder': _getString('time_of_order'),
'totalPassenger': _getString('total_passenger'),
'carType': _getString('car_type'),
'kazan': _getString('kazan'),
'startNameLocation': _getString('start_name_location'),
'endNameLocation': _getString('end_name_location'),
// --- Special Handling ---
// Steps (handle null values by providing an empty string)
'step0': _getString('step0'),
'step1': _getString('step1'),
'step2': _getString('step2'),
'step3': _getString('step3'),
'step4': _getString('step4'),
// Boolean conversion (1/0 from server to 'true'/'false' string for the app)
'WalletChecked': (serverData['wallet_checked'] == 1).toString(),
// Logic-based conversion for isHaveSteps
// Your app's `rideArguments` expects 'startEnd', so we provide that if has_steps is 1.
// You might need to adjust this logic if 'haveSteps' is also a possibility.
'isHaveSteps': (serverData['has_steps'] == 1)
? 'startEnd'
: 'noSteps', // Providing a default
};
}
void _sendAcceptanceNotification(String? customerToken, rideId) {
try {
if (customerToken == null) return;
List<String> bodyToPassenger = [
box.read(BoxName.driverID).toString(),
box.read(BoxName.nameDriver).toString(),
box.read(BoxName.tokenDriver).toString(),
rideId.toString()
];
// Safely check for customer token
final String? token = customerToken;
if (token != null && token.isNotEmpty) {
NotificationService.sendNotification(
target: token.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',
);
} else {}
} catch (e) {}
}
@@ -0,0 +1,67 @@
import 'dart:convert';
import 'dart:math';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class ZonesController extends GetxController {
Map<String, List<LatLng>> generateZoneMap(
LatLng southwest, LatLng southEast, LatLng northeast) {
const double desiredZoneArea = 4; // in square kilometers
final double width = (southEast.longitude - southwest.longitude) * 100;
final double height = (northeast.latitude - southEast.latitude) * 100;
final double totalArea = width * height;
// final int numZones = (totalArea / desiredZoneArea).ceil();
final double zoneWidth = width / sqrt(desiredZoneArea);
final double zoneHeight = height / sqrt(desiredZoneArea);
final numRows =
((northeast.latitude - southwest.latitude) / zoneHeight).ceil();
final numCols =
((southEast.longitude - southwest.longitude) / zoneWidth).ceil();
List<String> zoneNames = [];
List<LatLng> zoneCoordinates = [];
for (int row = 0; row < numRows; row++) {
for (int col = 0; col < numCols; col++) {
final double zoneSouthwestLat =
southwest.latitude + (row * zoneHeight / 100);
final double zoneSouthwestLng =
southwest.longitude + (col * zoneWidth / 100);
final double zoneNortheastLat = zoneSouthwestLat + zoneHeight / 100;
final double zoneNortheastLng = zoneSouthwestLng + zoneWidth / 100;
LatLng zoneSouthwest = LatLng(zoneSouthwestLat, zoneSouthwestLng);
LatLng zoneNortheast = LatLng(zoneNortheastLat, zoneNortheastLng);
String zoneName =
'Zone${row + col}'; // Assign a unique name to each zone
zoneNames.add(zoneName);
zoneCoordinates.add(zoneSouthwest);
zoneCoordinates.add(zoneNortheast);
}
}
Map<String, List<LatLng>> zoneMap = {};
for (int i = 0; i < zoneNames.length; i++) {
zoneMap[zoneNames[i]] = [
zoneCoordinates[i], // Southwest LatLng
zoneCoordinates[i + 1], // Northeast LatLng
];
}
return zoneMap;
}
void getJsonOfZones() {
LatLng southwest = const LatLng(32.111107, 36.062222);
LatLng southEast = const LatLng(32.108333, 36.101667);
LatLng northeast = const LatLng(32.143889, 36.058889);
Map<String, List<LatLng>> zoneMap =
generateZoneMap(southwest, southEast, northeast);
String jsonMap = json.encode(zoneMap);
}
}
+271
View File
@@ -0,0 +1,271 @@
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/controller/home/captin/help/maintain_center_controller.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// class MaintainCenterPage extends StatelessWidget {
// MaintainCenterPage({super.key});
// MaintainCenterController maintainCenterController =
// Get.put(MaintainCenterController());
// @override
// Widget build(BuildContext context) {
// return MyScafolld(
// title: "Maintenance Center".tr,
// body: [
// GetBuilder<MaintainCenterController>(
// builder: (maintainCenterController) {
// return Padding(
// padding: const EdgeInsets.all(8.0),
// child: Column(
// children: [
// Text(
// "When you complete 600 trips, you will be eligible to receive offers for maintenance of your car."
// .tr),
// const SizedBox(
// height: 10,
// ),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: [
// MyElevatedButton(
// title: "Show My Trip Count".tr,
// onPressed: () async {
// maintainCenterController.getTripCountByCaptain();
// }),
// _buildPriceAvatar(
// maintainCenterController.tripCount['count'] == null
// ? '0'
// : maintainCenterController.tripCount['count']
// .toString())
// ],
// ),
// const SizedBox(
// height: 10,
// ),
// Container(
// decoration: AppStyle.boxDecoration,
// child: Padding(
// padding: const EdgeInsets.all(14),
// child: Text(
// "We have maintenance offers for your car. You can use them after completing 600 trips to get a 20% discount on car repairs. Enjoy using our Tripz app and be part of our Tripz family."
// .tr,
// style: AppStyle.title,
// ),
// ),
// ),
// const SizedBox(
// height: 10,
// ),
// MyElevatedButton(
// title: 'Show maintenance center near my location'.tr,
// onPressed: () {
// if (maintainCenterController.tripCount['count'] > 600) {
// } else {
// Get.snackbar("You should complete 600 trips".tr, '',
// backgroundColor: AppColor.yellowColor);
// }
// })
// ],
// ),
// );
// })
// ],
// isleading: true);
// }
// Widget _buildPriceAvatar(String count) {
// return Container(
// width: 80,
// height: 80,
// decoration: BoxDecoration(
// shape: BoxShape.circle,
// gradient: const RadialGradient(
// colors: [Color(0xFF4CAF50), Color(0xFF2E7D32)],
// center: Alignment.center,
// radius: 0.8,
// ),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.2),
// blurRadius: 8,
// offset: const Offset(0, 4),
// ),
// ],
// ),
// child: Center(
// child: Text(
// count,
// style: const TextStyle(
// fontSize: 22,
// fontWeight: FontWeight.bold,
// color: Colors.white,
// ),
// ),
// ),
// );
// }
// }
class MaintainCenterPage extends StatelessWidget {
MaintainCenterPage({super.key});
final MaintainCenterController maintainCenterController =
Get.put(MaintainCenterController());
@override
Widget build(BuildContext context) {
return MyScafolld(
title: "Maintenance Center".tr,
body: [
GetBuilder<MaintainCenterController>(
builder: (controller) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Introduction Text with better styling
Text(
"When you complete 600 trips, you will be eligible to receive offers for maintenance of your car."
.tr,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Colors.grey[700],
height: 1.4,
),
),
const SizedBox(height: 20),
// Trip Count Section in a Card
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyElevatedButton(
title: "Show My Trip Count".tr,
onPressed: () async {
controller.getTripCountByCaptain();
},
),
),
const SizedBox(width: 16),
_buildPriceAvatar(
controller.tripCount['count'] == null
? '0'
: controller.tripCount['count'].toString(),
),
],
),
),
),
const SizedBox(height: 20),
// Maintenance Offer Information in a Card
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Maintenance Offer".tr,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
"We have maintenance offers for your car. You can use them after completing 600 trips to get a 20% discount on car repairs. Enjoy using our Tripz app and be part of our Tripz family."
.tr,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: Colors.grey[800],
height: 1.5,
),
),
],
),
),
),
const SizedBox(height: 20),
// Show Maintenance Center Button
SizedBox(
width: double.infinity,
child: MyElevatedButton(
title: 'Show maintenance center near my location'.tr,
onPressed: () {
if (controller.tripCount['count'] != null &&
controller.tripCount['count'] >= 600) {
// Implement navigation or action to show maintenance centers
// For now, let's print a message
print("Navigating to maintenance centers...");
} else {
Get.snackbar(
"Ineligible for Offer".tr,
"You should complete 500 trips to unlock this feature."
.tr,
backgroundColor: AppColor.yellowColor,
colorText: Colors.black,
snackPosition: SnackPosition.BOTTOM,
);
}
},
),
),
],
),
);
},
),
],
isleading: true,
);
}
Widget _buildPriceAvatar(String count) {
return Container(
width: 70, // Slightly reduced size
height: 70,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
// Changed to LinearGradient
colors: [Color(0xFF4CAF50), Color(0xFF2E7D32)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15), // Reduced opacity
blurRadius: 6, // Slightly reduced blur
offset: const Offset(0, 3),
),
],
),
child: Center(
child: Text(
count,
style: const TextStyle(
fontSize: 20, // Slightly reduced size
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}
}
@@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:slide_to_act/slide_to_act.dart';
import '../../../../constant/colors.dart';
import '../../../../controller/home/captin/map_driver_controller.dart';
Widget driverEndRideBar() {
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 : -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]
}
/// دالة تنسيق المدة:
/// - أقل من ساعة: mm:ss
/// - ساعة فأكثر: h:mm:ss (خانة واحدة للساعات بدون leading zero)
String _formatDurationFromStart(MapDriverController controller) {
if (controller.rideStartTime == null) return "00:00";
final d = DateTime.now().difference(controller.rideStartTime!);
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],
),
),
]
],
),
],
),
);
}
Widget _buildVerticalDivider() {
return Container(height: 35, width: 1, color: Colors.grey.shade300);
}
@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../../../../controller/functions/location_controller.dart';
import '../../../../controller/home/captin/map_driver_controller.dart';
class GoogleDriverMap extends StatelessWidget {
const GoogleDriverMap({
super.key,
required this.locationController,
});
final LocationController locationController;
@override
Widget build(BuildContext context) {
final MapDriverController controller = Get.put(MapDriverController());
// New: تحديد قيمة الـ padding لتحريك مركز الخريطة للأعلى
final double mapPaddingBottom = MediaQuery.of(context).size.height * 0.3;
return GetBuilder<MapDriverController>(
builder: (controller) => GoogleMap(
onMapCreated: (mapController) {
controller.onMapCreated(mapController);
// New: تطبيق الـ padding بعد إنشاء الخريطة مباشرة
mapController.setMapStyle('[]'); // يمكنك إضافة تصميم مخصص للخريطة هنا
// يمكنك استخدام CameraUpdate.scrollBy لتحريك الكاميرا إذا رغبت بذلك:
// controller.mapController!.animateCamera(CameraUpdate.scrollBy(0, mapPaddingBottom));
},
// New: إضافة padding لتحريك مركز الخريطة للأعلى، مما يجعل أيقونة السائق تظهر في الأسفل
zoomControlsEnabled: false, // Changed: تم إخفاء أزرار الزوم الافتراضية
initialCameraPosition: CameraPosition(
target: locationController.myLocation,
zoom: 17,
bearing: locationController.heading, // استخدام اتجاه السائق
tilt: 60, // زاوية ميل
),
onCameraMove: (position) {
CameraPosition(
target: locationController.myLocation,
zoom: 17.5,
tilt: 50.0,
bearing: locationController.heading,
);
},
padding: EdgeInsets.only(bottom: 50, top: Get.height * 0.7),
minMaxZoomPreference: const MinMaxZoomPreference(8, 18),
myLocationEnabled: false, // Changed: تم الاعتماد على ماركر مخصص
myLocationButtonEnabled: true,
compassEnabled: true,
mapType: MapType.terrain,
trafficEnabled: true, // Changed: تفعيل عرض حركة المرور
buildingsEnabled: true,
polylines: {
// 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,
// ),
// Polyline(
// zIndex: 2,
// polylineId: const PolylineId('route'),
// points: controller.polylineCoordinatesDestination,
// color: const Color.fromARGB(255, 10, 29, 126),
// width: 6, // Changed: زيادة عرض الخط
// startCap: Cap.roundCap,
// endCap: Cap.roundCap,
// ),
Polyline(
polylineId: const PolylineId('upcoming_route'),
points: controller.upcomingPathPoints,
color: Colors.blue, // أو أي لون آخر تختاره للمسار
width: 8,
zIndex: 2,
),
// 2. الخط المقطوع (تحت)
Polyline(
polylineId: const PolylineId('traveled_route'),
points: controller.traveledPathPoints,
color: Colors.grey.withOpacity(0.8),
width: 7,
zIndex: 1,
),
},
markers: {
Marker(
markerId: MarkerId('MyLocation'.tr),
position: locationController.myLocation,
rotation: locationController.heading,
flat: true,
anchor: const Offset(0.5, 0.5),
icon: controller.carIcon,
),
Marker(
markerId: MarkerId('start'.tr),
position: controller.latLngPassengerLocation,
icon: controller.startIcon,
),
Marker(
markerId: MarkerId('end'.tr),
position: controller.latLngPassengerDestination,
icon: controller.endIcon,
),
},
),
);
}
}
@@ -0,0 +1,62 @@
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
class GoogleMapApp extends StatelessWidget {
const GoogleMapApp({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MapDriverController>(
builder: (mapDriverController) {
if (!mapDriverController.isRideStarted) return const SizedBox();
// 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';
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,
),
),
),
);
},
);
}
}
@@ -0,0 +1,392 @@
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 '../../../../constant/style.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 {
const PassengerInfoWindow({super.key});
@override
Widget build(BuildContext context) {
// 1. حساب الهوامش الآمنة (SafeArea) من الأسفل
final double safeBottomPadding = MediaQuery.of(context).padding.bottom;
return GetBuilder<MapDriverController>(
// 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: [
// --- مقبض السحب (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),
),
),
),
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),
// النصوص (الاسم والمسافة)
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),
),
],
),
],
),
),
// أزرار جانبية (سرعة + اتصال)
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)),
),
),
),
],
),
),
],
),
),
);
},
);
}
// --- 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),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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 _buildWaitingIndicator(MapDriverController controller) {
bool isUrgent = controller.remainingTimeInPassengerLocatioWait < 60;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
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(
controller.stringRemainingTimeWaitingPassenger,
style: TextStyle(
fontWeight: FontWeight.w900,
color: isUrgent ? Colors.red : Colors.green,
fontFamily: 'monospace'),
),
],
),
);
}
Widget _buildActionButtons(MapDriverController controller) {
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)),
),
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();
},
);
},
icon: const Icon(Icons.play_circle_fill_rounded),
label: Text('Start Ride'.tr,
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
);
}
}
}
@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:get/get.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/notification_service.dart';
import '../../../../controller/functions/launch.dart';
import '../../../../controller/home/captin/map_driver_controller.dart';
import '../../../../main.dart';
class SosConnect extends StatelessWidget {
SosConnect({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MapDriverController>(
id: 'SosConnect', // Keep ID for updates
builder: (controller) {
// 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: [
// === Call Button ===
if (showPassengerContact)
_buildModernActionButton(
icon: Icons.phone_in_talk,
color: Colors.white,
bgColor: AppColor.blueColor,
tooltip: 'Call Passenger',
onTap: () async {
controller.isSocialPressed = true;
bool canCall = await controller.driverCallPassenger();
if (canCall) {
makePhoneCall(controller.passengerPhone.toString());
} else {
mySnackeBarError("Policy restriction on calls".tr);
}
},
),
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 Button ===
if (showSos)
_buildModernActionButton(
icon: MaterialIcons.warning,
color: Colors.white,
bgColor: AppColor.redColor,
tooltip: 'EMERGENCY SOS',
isPulsing: true,
onTap: () => _handleSosCall(controller),
),
],
),
);
},
);
}
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: '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 & Call'.tr,
onPressed: () {
if (mapDriverController.formKey1.currentState!.validate()) {
box.write(BoxName.sosPhoneDriver,
mapDriverController.sosEmergincyNumberCotroller.text);
Get.back();
launchCommunication(
'phone', box.read(BoxName.sosPhoneDriver), '');
}
},
),
);
} else {
launchCommunication('phone', box.read(BoxName.sosPhoneDriver), '');
}
}
void _showMessageOptions(
BuildContext context, MapDriverController controller) {
Get.bottomSheet(
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: [
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(
icon: const Icon(Icons.send),
onPressed: () {
_sendMessage(controller, controller.messageToPassenger.text,
'cancel');
controller.messageToPassenger.clear();
Get.back();
},
),
],
),
],
),
),
);
}
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',
);
}
}
@@ -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;
}
}
+25
View File
@@ -0,0 +1,25 @@
// import 'package:flutter/material.dart';
// import 'package:ride/constant/api_key.dart';
// import 'package:ride/constant/box_name.dart';
// import 'package:ride/main.dart';
// import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart';
// class CallPage extends StatelessWidget {
// const CallPage({Key? key, required this.callID}) : super(key: key);
// final String callID;
// @override
// Widget build(BuildContext context) {
// return ZegoUIKitPrebuiltCall(
// appID: AK
// .zegoCloudAppID, // Fill in the appID that you get from ZEGOCLOUD Admin Console.
// appSign: AK
// .zegoCloudAppSIGN, // Fill in the appSign that you get from ZEGOCLOUD Admin Console.
// userID: box.read(BoxName.passengerID) ?? box.read(BoxName.driverID),
// userName: box.read(BoxName.name) ?? box.read(BoxName.nameDriver),
// callID: callID,
// // You can also use groupVideo/groupVoice/oneOnOneVoice to make more types of calls.
// config: ZegoUIKitPrebuiltCallConfig.oneOnOneVoiceCall(),
// );
// }
// }
@@ -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());
}
}
@@ -0,0 +1,814 @@
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';
import '../../../../controller/firebase/local_notification.dart';
import '../../../../controller/firebase/notification_service.dart';
import '../../../../controller/functions/crud.dart';
import '../../../../main.dart';
import '../../../../models/model/order_data.dart';
import '../../../../print.dart';
// === Enhanced Colors for Better Readability ===
class AppColors {
static const primary = Color(0xFF1A252F);
static const card = Color(0xFF2C3E50);
static const white = Colors.white;
static const gray = Color(0xFFBDC3C7);
static const lightGray = Color(0xFFECF0F1);
static const accent = Color(0xFF00BCD4);
static const accept = Color(0xFF4CAF50);
static const reject = Color(0xFFFF5722);
static const highlight = Color(0xFFFFC107);
static const priceHighlight = Color(0xFF00E676);
static const urgentRed = Color(0xFFD32F2F);
}
class OrderOverlay extends StatefulWidget {
const OrderOverlay({Key? key}) : super(key: key);
@override
State<OrderOverlay> createState() => _OrderOverlayState();
}
class _OrderOverlayState extends State<OrderOverlay>
with WidgetsBindingObserver {
// === State Variables ===
OrderData? orderData;
Timer? timer;
int remainingSeconds = 10;
final AudioPlayer audioPlayer = AudioPlayer();
bool buttonsEnabled = true;
final String mapApiKey = AK.mapAPIKEY;
final CRUD _crud = CRUD();
final NotificationController notificationController =
Get.put(NotificationController());
// === Getters ===
bool get canShowMap {
if (orderData == null || mapApiKey.isEmpty) return false;
final start = orderData!.startCoordinates;
final end = orderData!.endCoordinates;
return start?['lat'] != null &&
start?['lng'] != null &&
end?['lat'] != null &&
end?['lng'] != null;
}
String get staticMapUrl {
if (!canShowMap) return "";
final start = orderData!.startCoordinates!;
final end = orderData!.endCoordinates!;
final startMarker = Uri.encodeComponent("${start['lat']},${start['lng']}");
final endMarker = Uri.encodeComponent("${end['lat']},${end['lng']}");
return "https://maps.googleapis.com/maps/api/staticmap?"
"size=600x150&maptype=roadmap"
"&markers=color:green%7Clabel:S%7C$startMarker"
"&markers=color:red%7Clabel:D%7C$endMarker"
"&path=color:0x007bff%7Cweight:5%7C$startMarker%7C$endMarker"
"&key=$mapApiKey";
}
// === Lifecycle ===
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
FlutterOverlayWindow.overlayListener.listen((event) {
if (mounted) _processEventData(event);
});
}
@override
void dispose() {
timer?.cancel();
_stopAudio();
audioPlayer.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_checkOverlayStatus();
}
}
List myList = [];
// === Setup & Listeners ===
void _setupOverlayListener() {
FlutterOverlayWindow.overlayListener.listen((event) {
if (mounted) _processEventData(event);
});
}
void _processEventData(dynamic event) {
_log("Received event: $event");
if (event is List<dynamic>) {
try {
myList = event;
final newOrder = OrderData.fromList(event);
_log("Parsed OrderData: ${newOrder.toMap()}");
setState(() {
orderData = newOrder;
});
_resetAndStartTimer();
} catch (e, s) {
_log("Error parsing OrderData: $e\nStackTrace: $s");
}
} else {
_log("Unexpected data format: $event");
}
}
void _checkOverlayStatus() async {
bool isActive = await FlutterOverlayWindow.isActive();
if (isActive && mounted && orderData != null) {
if (remainingSeconds > 0 && (timer == null || !timer!.isActive)) {
_resetAndStartTimer();
}
}
}
// === Timer Management ===
void _resetAndStartTimer() {
timer?.cancel();
audioPlayer.stop();
setState(() {
buttonsEnabled = true;
remainingSeconds = _calculateTimerDuration();
});
_playAudio();
_startTimer();
}
int _calculateTimerDuration() {
if (orderData?.durationToPassengerMinutes != null &&
orderData!.durationToPassengerMinutes > 0) {
int duration = orderData!.durationToPassengerMinutes * 60;
return duration > 10 ? 10 : duration;
}
return 10;
}
void _startTimer() {
if (orderData == null) return;
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) {
timer.cancel();
_stopAudio();
return;
}
setState(() {
if (remainingSeconds > 0) {
remainingSeconds--;
} else {
timer.cancel();
_stopAudio();
if (buttonsEnabled) _handleOrderTimeout();
}
});
});
}
// === Audio Management ===
void _playAudio() async {
try {
await audioPlayer.setAsset('assets/order.mp3', preload: true);
await audioPlayer.setLoopMode(LoopMode.one);
await audioPlayer.play();
} catch (e) {
_log('Error playing audio: $e');
}
}
void _stopAudio() {
audioPlayer.stop();
}
String _getData(int index, {String defaultValue = ''}) {
if (myList.length > index && myList[index] != null) {
return myList[index].toString();
}
return defaultValue;
}
// === Order Actions ===
Future<void> _acceptOrder() async {
if (!buttonsEnabled || orderData == null) return;
_disableButtonsAndProcess();
_log("Order ACCEPTED: ${orderData!.orderId}");
try {
final driverId = box.read(BoxName.driverID)?.toString();
if (driverId == null) {
_log("Error: Driver ID is null. Closing overlay.");
await _closeOverlay();
return;
}
var res = await CRUD()
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
'id': orderData!.orderId,
'rideTimeStart': DateTime.now().toString(),
'status': 'Apply',
'driver_id': box.read(BoxName.driverID),
'passengerToken': _getData(9),
});
final payload = {
// بيانات أساسية
'driver_id': driverId,
'status': 'Apply',
'passengerLocation': _getData(0),
'passengerDestination': _getData(1),
'Duration': _getData(4),
'totalCost': _getData(26),
'Distance': _getData(5),
'name': _getData(8),
'phone': _getData(10),
'email': _getData(28),
'WalletChecked': _getData(13),
'tokenPassenger': _getData(9),
'direction': staticMapUrl.toString(),
'DurationToPassenger': _getData(15),
'rideId': orderData!.orderId,
'passengerId': _getData(7),
'durationOfRideValue': _getData(19),
'paymentAmount': _getData(2),
'paymentMethod': _getData(13) == 'true' ? 'visa' : 'cash',
'isHaveSteps': _getData(20),
'step0': myList[21].toString(),
'step1': myList[22].toString(),
'step2': myList[23].toString(),
'step3': myList[24].toString(),
'step4': myList[25].toString(),
'passengerWalletBurc': myList[26].toString(),
'carType': myList[31].toString(),
'kazan': myList[32].toString(),
'startNameLocation': myList[29].toString(),
'endNameLocation': myList[30].toString(),
// الحقول الإضافية التي يجب تضمينها
'timeOfOrder': DateTime.now().toIso8601String(),
'totalPassenger': _getData(2),
};
Log.print('payload: ${payload}');
CRUD().post(
link: AppLink.addOverLayStatus,
payload: payload,
);
if (res != "failure") {
// Using rideId (_getData(16)) for order_id consistently
_log("Server update successful. Writing to storage.");
notificationController.showNotification(
"Order Accepted".tr,
"Open app and go to passenger".tr,
'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");
notificationController.showNotification(
"Order Accepted by another driver".tr,
"Open app and go to passenger".tr,
'ding',
'',
);
await _closeOverlay();
}
} catch (e, s) {
_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;
}
}
// Your list parsing for 'customerToken' should be something like:
// customerToken: list.length > a_certain_index ? list[a_certain_index].toString() : null,
Future<void> _rejectOrder() async {
if (!buttonsEnabled || orderData == null) return;
_disableButtonsAndProcess();
_log("Order REJECTED: ${orderData!.orderId}");
box.write(BoxName.rideStatus, 'reject');
Log.print('rideStatus from overlay 303 : ${box.read(BoxName.rideStatus)}');
await _apiRefuseOrder(orderData!.orderId);
await _closeOverlay();
}
void _handleOrderTimeout() {
if (orderData == null) return;
_log("Order TIMED OUT: ${orderData!.orderId}");
_rejectOrder();
}
Future<void> _apiRefuseOrder(String orderID) async {
if (orderID == "N/A") {
_log("Cannot refuse order with N/A ID");
return;
}
try {
final driverId = box.read(BoxName.driverID)?.toString();
if (driverId == null) {
_log("Driver ID is null, cannot refuse order");
return;
}
CRUD().post(link: AppLink.addDriverOrder, payload: {
'driver_id': driverId,
'order_id': orderID,
'status': 'Refused'
});
_log("Order $orderID refused successfully");
} catch (e) {
_log("Error in _apiRefuseOrder for $orderID: $e");
}
}
// === Helper Methods ===
void _disableButtonsAndProcess() {
setState(() => buttonsEnabled = false);
timer?.cancel();
_stopAudio();
}
Future<void> _closeOverlay() async {
_stopAudio();
timer?.cancel();
if (await FlutterOverlayWindow.isActive()) {
await FlutterOverlayWindow.closeOverlay();
}
}
void _log(String message) {
// A simple logger to distinguish overlay logs
print("OVERLAY_LOG: $message");
}
// === UI Build Methods ===
@override
Widget build(BuildContext context) {
// ... (Your entire UI build method remains unchanged) ...
// The UI code is excellent and doesn't need modification.
if (orderData == null) {
return const Material(
color: Colors.transparent,
child: Center(
child: CircularProgressIndicator(color: AppColors.accent)));
}
return Material(
color: Colors.black.withOpacity(0.4),
child: Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.circular(20.0),
border: Border.all(
color: AppColors.accent.withOpacity(0.3), width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.6),
blurRadius: 15,
spreadRadius: 2,
)
],
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildQuickHeader(),
const SizedBox(height: 12),
_buildPrimaryInfo(),
const SizedBox(height: 12),
if (canShowMap) _buildCompactMap(),
if (canShowMap) const SizedBox(height: 12),
_buildSecondaryInfo(),
const SizedBox(height: 16),
_buildEnhancedActionButtons(),
],
),
),
),
),
);
}
// All your _build... widget methods (_buildQuickHeader, _buildPrimaryInfo, etc.)
// are perfectly fine and do not need to be changed.
// ... Paste all your existing _build... methods here ...
Widget _buildQuickHeader() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
remainingSeconds <= 3
? AppColors.urgentRed
: remainingSeconds <= 5
? AppColors.highlight
: AppColors.accent,
remainingSeconds <= 3
? AppColors.urgentRed.withOpacity(0.7)
: remainingSeconds <= 5
? AppColors.highlight.withOpacity(0.7)
: AppColors.accent.withOpacity(0.7),
],
),
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.drive_eta_rounded, color: AppColors.white, size: 24),
const SizedBox(width: 8),
Text(
"طلب جديد".tr,
style: const TextStyle(
color: AppColors.white,
fontSize: 18,
fontWeight: FontWeight.w600),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: AppColors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(20),
),
child: Text(
"$remainingSeconds ث",
style: TextStyle(
color: remainingSeconds <= 3
? AppColors.urgentRed
: remainingSeconds <= 5
? AppColors.highlight
: AppColors.accent,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
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(
color: AppColors.primary.withOpacity(0.6),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.priceHighlight.withOpacity(0.3), width: 1),
),
child: Column(
children: [
// Price and Distance - Most Important Info
Row(
children: [
Expanded(
flex: 3,
child: _buildHighlightInfo(
// FIX: Use the parsed priceValue here
"${NumberFormat('#,##0').format(priceValue)} ل.س",
"السعر".tr,
Icons.monetization_on_rounded,
AppColors.priceHighlight,
isLarge: true,
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: _buildHighlightInfo(
// Ensure tripDistanceKm is treated safely too
"${(num.tryParse(order.tripDistanceKm.toString()) ?? 0).toStringAsFixed(1)} كم",
"المسافة".tr,
Icons.straighten_rounded,
AppColors.accent,
),
),
],
),
const SizedBox(height: 12),
Divider(color: AppColors.gray.withOpacity(0.2), thickness: 1),
const SizedBox(height: 12),
// Passenger Info and ETA
Row(
children: [
Expanded(
flex: 2,
child: _buildPassengerQuickInfo(),
),
const SizedBox(width: 12),
Expanded(
child: _buildHighlightInfo(
"${order.durationToPassengerMinutes} د",
"للوصول".tr,
Icons.access_time_filled_rounded,
order.durationToPassengerMinutes <= 3
? AppColors.priceHighlight
: AppColors.gray,
),
),
],
),
],
),
);
}
Widget _buildHighlightInfo(
String value, String label, IconData icon, Color color,
{bool isLarge = false}) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: color.withOpacity(0.3), width: 1),
),
child: Column(
children: [
Icon(icon, color: color, size: isLarge ? 24 : 20),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
color: AppColors.white,
fontSize: isLarge ? 20 : 16,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
Text(
label,
style: TextStyle(
color: AppColors.lightGray.withOpacity(0.7),
fontSize: 11,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildPassengerQuickInfo() {
final order = orderData!;
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: AppColors.highlight.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border:
Border.all(color: AppColors.highlight.withOpacity(0.3), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.person_rounded, color: AppColors.highlight, size: 18),
const SizedBox(width: 4),
Expanded(
child: Text(
order.customerName,
style: const TextStyle(
color: AppColors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 2),
Text(
order.rideType,
style: TextStyle(
color: AppColors.lightGray.withOpacity(0.7),
fontSize: 11,
),
),
],
),
);
}
Widget _buildCompactMap() {
return ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Image.network(
staticMapUrl,
height: 100, // Reduced from 110
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
height: 100,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child:
Icon(Icons.map_outlined, color: AppColors.gray, size: 32)),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return SizedBox(
height: 100,
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
color: AppColors.accent,
strokeWidth: 2.0,
),
),
);
},
),
);
}
Widget _buildSecondaryInfo() {
final order = orderData!;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.4),
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: [
_buildLocationRow(
Icons.trip_origin_rounded,
"من".tr,
order.startLocationAddress,
Colors.green.shade300,
),
const SizedBox(height: 8),
_buildLocationRow(
Icons.flag_rounded,
"إلى".tr,
order.endLocationAddress,
Colors.red.shade300,
),
if (order.tripDurationMinutes > 0) ...[
const SizedBox(height: 8),
Divider(color: AppColors.gray.withOpacity(0.2), thickness: 0.5),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.timer_outlined, color: AppColors.accent, size: 16),
const SizedBox(width: 4),
Text(
"مدة الرحلة: ${order.tripDurationMinutes} دقيقة".tr,
style: const TextStyle(
color: AppColors.lightGray,
fontSize: 12,
),
),
],
),
]
],
),
);
}
Widget _buildLocationRow(
IconData icon, String label, String address, Color iconColor) {
return Row(
children: [
Icon(icon, color: iconColor, size: 16),
const SizedBox(width: 8),
Text(
"$label: ",
style: TextStyle(
color: AppColors.lightGray.withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
Expanded(
child: Text(
address,
style: const TextStyle(
color: AppColors.white,
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
);
}
Widget _buildEnhancedActionButtons() {
return Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: buttonsEnabled ? _rejectOrder : null,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.reject,
foregroundColor: AppColors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
disabledBackgroundColor: AppColors.reject.withOpacity(0.3),
elevation: 3,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close_rounded, size: 20),
const SizedBox(width: 6),
Text(
"رفض".tr,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
),
],
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: buttonsEnabled ? _acceptOrder : null,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accept,
foregroundColor: AppColors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
disabledBackgroundColor: AppColors.accept.withOpacity(0.3),
elevation: 3,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle_rounded, size: 20),
const SizedBox(width: 6),
Text(
"قبول".tr,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
),
],
),
),
),
],
);
}
}
@@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/controller/home/captin/order_request_controller.dart';
class OrderRequestPage extends StatelessWidget {
const OrderRequestPage({super.key});
@override
Widget build(BuildContext context) {
// حقن الكنترولر
final OrderRequestController controller = Get.put(OrderRequestController());
return Scaffold(
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();
},
),
),
// 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),
),
],
),
),
),
),
// 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)
],
),
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2)))),
const SizedBox(height: 15),
// الصف الأول: الراكب والسعر
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const CircleAvatar(
radius: 24,
backgroundColor: Color(0xFFF5F5F5),
child: Icon(Icons.person,
color: Colors.grey, size: 28),
),
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),
],
),
),
],
),
],
),
const SizedBox(height: 15),
// الصف الثاني: شريط المعلومات
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)),
],
),
),
),
],
),
],
),
),
),
],
);
},
),
),
);
}
Widget _buildInfoItem(IconData icon, String text, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 6),
Text(text,
style: TextStyle(
fontSize: 13, fontWeight: FontWeight.bold, color: color)),
],
);
}
}
@@ -0,0 +1,558 @@
// import 'dart:convert';
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:google_maps_flutter/google_maps_flutter.dart';
// import 'package:sefer_driver/controller/home/captin/home_captain_controller.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/print.dart';
// import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
// 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/launch.dart';
// import '../../../../controller/home/captin/order_request_controller.dart';
// import '../../../widgets/elevated_btn.dart';
// import '../../../widgets/mydialoug.dart';
// class OrderSpeedRequest extends StatelessWidget {
// OrderSpeedRequest({super.key});
// final OrderRequestController orderRequestController =
// Get.put(OrderRequestController());
// // دالة مساعدة لاستخراج البيانات بأمان (Null Safety)
// String _getData(int index, {String defaultValue = ''}) {
// // if (orderRequestController.myList.length > index &&
// // orderRequestController.myList[index] != null) {
// // return orderRequestController.myList[index].toString();
// // }
// return defaultValue;
// }
// @override
// Widget build(BuildContext context) {
// return GetBuilder<OrderRequestController>(
// builder: (controller) {
// // --- استخراج البيانات بشكل نظيف ---
// final String price =
// double.tryParse(_getData(2))?.toStringAsFixed(2) ?? 'N/A';
// final bool isComfortTrip = _getData(31) == 'Comfort';
// final String carType = _getData(31).tr;
// final String pickupName = _getData(12);
// final String pickupDetails = '(${_getData(11)})';
// final String pickupFullAddress = _getData(29);
// final String dropoffName = _getData(5);
// final String dropoffDetails = '(${_getData(4)})';
// final String dropoffFullAddress = _getData(30);
// final String passengerName = _getData(8);
// final String passengerRating = _getData(33);
// final bool isVisaPayment = _getData(13) == 'true';
// final bool hasSteps = _getData(20) == 'haveSteps';
// final String mapUrl =
// 'https://www.google.com/maps/dir/${_getData(0)}/${_getData(1)}/';
// final String rideId = _getData(16);
// return Scaffold(
// appBar: AppBar(
// title: Text('Speed Order'.tr),
// leading: IconButton(
// icon: const Icon(Icons.arrow_back),
// onPressed: () => Get.back(),
// ),
// backgroundColor: AppColor.primaryColor,
// elevation: 2.0,
// ),
// backgroundColor: AppColor.secondaryColor ?? Colors.grey[100],
// body: SafeArea(
// child: Padding(
// padding:
// const EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0),
// child: Column(
// children: [
// // 1. قسم الخريطة (ارتفاع ثابت)
// SizedBox(
// height: Get.height * 0.28,
// child: ClipRRect(
// borderRadius: BorderRadius.circular(15.0),
// child: GoogleMap(
// initialCameraPosition: CameraPosition(
// zoom: 12,
// target: Get.find<HomeCaptainController>().myLocation,
// ),
// cameraTargetBounds:
// CameraTargetBounds(controller.bounds),
// myLocationButtonEnabled: false,
// trafficEnabled: false,
// buildingsEnabled: false,
// mapToolbarEnabled: false,
// myLocationEnabled: true,
// markers: {
// Marker(
// markerId: MarkerId('MyLocation'.tr),
// position: LatLng(controller.latPassengerLocation,
// controller.lngPassengerLocation),
// icon: controller.startIcon),
// Marker(
// markerId: MarkerId('Destination'.tr),
// position: LatLng(
// controller.latPassengerDestination,
// controller.lngPassengerDestination),
// icon: controller.endIcon),
// },
// polylines: {
// Polyline(
// zIndex: 1,
// consumeTapEvents: true,
// geodesic: true,
// endCap: Cap.buttCap,
// startCap: Cap.buttCap,
// visible: true,
// polylineId: const PolylineId('routeOrder'),
// points: controller.pointsDirection,
// color: AppColor.primaryColor,
// width: 3,
// ),
// },
// ),
// ),
// ),
// const SizedBox(height: 8),
// // 2. بطاقة السعر
// _buildPriceCard(price, carType, isComfortTrip),
// const SizedBox(height: 8),
// // 3. التفاصيل القابلة للتمرير (تأخذ المساحة المتبقية)
// Expanded(
// child: SingleChildScrollView(
// physics: const BouncingScrollPhysics(),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// _buildLocationCard(
// icon: Icons.arrow_circle_up,
// iconColor: AppColor.greenColor,
// title: pickupName,
// subtitle: pickupDetails,
// fullAddress: pickupFullAddress,
// ),
// const SizedBox(height: 8),
// _buildLocationCard(
// icon: Icons.arrow_circle_down,
// iconColor: AppColor.redColor,
// title: dropoffName,
// subtitle: dropoffDetails,
// fullAddress: dropoffFullAddress,
// ),
// const SizedBox(height: 8),
// _buildInfoCard(isVisaPayment, hasSteps, mapUrl),
// const SizedBox(height: 8),
// _buildPassengerCard(passengerName, passengerRating),
// ],
// ),
// ),
// ),
// // 4. الأزرار والمؤقت (مثبتة في الأسفل)
// Padding(
// padding: const EdgeInsets.only(top: 8.0),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// // زر القبول
// Expanded(
// child: MyElevatedButton(
// kolor: AppColor.greenColor,
// title: 'Accept Order'.tr,
// onPressed: () => _handleAcceptOrder(controller),
// ),
// ),
// const SizedBox(width: 10),
// // المؤقت
// GetBuilder<OrderRequestController>(
// id: 'timerUpdate',
// builder: (timerCtrl) {
// final isNearEnd = timerCtrl.remainingTimeSpeed <= 5;
// return SizedBox(
// width: 60,
// height: 60,
// child: Stack(
// alignment: Alignment.center,
// children: [
// CircularProgressIndicator(
// value: timerCtrl.progressSpeed,
// color: isNearEnd
// ? Colors.redAccent
// : AppColor.primaryColor,
// strokeWidth: 5,
// backgroundColor: Colors.grey.shade300,
// ),
// Text(
// '${timerCtrl.remainingTimeSpeed}',
// style: AppStyle.headTitle2.copyWith(
// color: isNearEnd
// ? Colors.redAccent
// : AppColor.writeColor ?? Colors.black,
// ),
// ),
// ],
// ),
// );
// },
// ),
// const SizedBox(width: 10),
// // زر الرفض
// Expanded(
// child: MyElevatedButton(
// title: 'Refuse Order'.tr,
// onPressed: () =>
// _handleRefuseOrder(controller, rideId),
// kolor: AppColor.redColor,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
// ),
// );
// },
// );
// }
// // --- WIDGET BUILDERS (لبناء الواجهة بشكل نظيف) ---
// // Widget _buildPriceCard(String price, String carType, bool isComfortTrip) {
// // return Card(
// // elevation: 3,
// // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
// // child: Padding(
// // padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.spaceBetween,
// // children: [
// // Text(
// // price,
// // style: AppStyle.headTitle.copyWith(
// // color: AppColor.primaryColor,
// // fontWeight: FontWeight.bold,
// // fontSize: 28),
// // ),
// // Column(
// // crossAxisAlignment: CrossAxisAlignment.end,
// // children: [
// // Text(
// // carType,
// // style: AppStyle.title.copyWith(
// // color: AppColor.greenColor, fontWeight: FontWeight.bold),
// // ),
// // if (isComfortTrip)
// // Row(
// // children: [
// // const Icon(Icons.ac_unit,
// // color: AppColor.blueColor, size: 18),
// // const SizedBox(width: 4),
// // Text('Air condition Trip'.tr,
// // style: AppStyle.subtitle.copyWith(fontSize: 13)),
// // ],
// // ),
// // ],
// // ),
// // ],
// // ),
// // ),
// // );
// // }
// // Widget _buildInfoCard(bool isVisaPayment, bool hasSteps, String mapUrl) {
// // return Card(
// // elevation: 2,
// // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
// // child: Padding(
// // padding: const EdgeInsets.all(10.0),
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.spaceBetween,
// // children: [
// // Row(
// // children: [
// // Icon(
// // isVisaPayment ? Icons.credit_card : Icons.payments_outlined,
// // color: isVisaPayment
// // ? AppColor.deepPurpleAccent
// // : AppColor.greenColor,
// // size: 24,
// // ),
// // const SizedBox(width: 8),
// // Text(
// // isVisaPayment ? 'Visa'.tr : 'Cash'.tr,
// // style: AppStyle.title.copyWith(fontWeight: FontWeight.w600),
// // ),
// // ],
// // ),
// // if (hasSteps)
// // Expanded(
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.center,
// // children: [
// // const Icon(Icons.format_list_numbered_rtl_outlined,
// // color: AppColor.bronze, size: 24),
// // const SizedBox(width: 4),
// // Flexible(
// // child: Text(
// // 'Trip has Steps'.tr,
// // style: AppStyle.title
// // .copyWith(color: AppColor.bronze, fontSize: 13),
// // overflow: TextOverflow.ellipsis,
// // )),
// // ],
// // ),
// // ),
// // TextButton.icon(
// // style: TextButton.styleFrom(
// // padding: EdgeInsets.zero,
// // tapTargetSize: MaterialTapTargetSize.shrinkWrap,
// // alignment: Alignment.centerRight,
// // ),
// // onPressed: () => showInBrowser(mapUrl),
// // icon: const Icon(Icons.directions_outlined,
// // color: AppColor.blueColor, size: 20),
// // label: Text("Directions".tr,
// // style: AppStyle.subtitle
// // .copyWith(color: AppColor.blueColor, fontSize: 13)),
// // ),
// // ],
// // ),
// // ),
// // );
// // }
// // Widget _buildPassengerCard(String name, String rating) {
// // return Card(
// // elevation: 2,
// // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
// // child: Padding(
// // padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
// // child: Row(
// // children: [
// // const Icon(Icons.person_outline,
// // color: AppColor.greyColor, size: 22),
// // const SizedBox(width: 10),
// // Expanded(
// // child: Text(
// // name,
// // style: AppStyle.title,
// // overflow: TextOverflow.ellipsis,
// // ),
// // ),
// // const SizedBox(width: 10),
// // const Icon(Icons.star_rounded, color: Colors.amber, size: 20),
// // Text(
// // rating,
// // style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
// // ),
// // ],
// // ),
// // ),
// // );
// // }
// // Widget _buildLocationCard(
// {required IconData icon,
// required Color iconColor,
// required String title,
// required String subtitle,
// required String fullAddress}) {
// return Card(
// elevation: 2,
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
// margin: const EdgeInsets.symmetric(vertical: 4),
// child: Padding(
// padding: const EdgeInsets.all(10.0),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Icon(icon, color: iconColor, size: 28),
// const SizedBox(width: 12),
// Expanded(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// "$title $subtitle".trim(),
// style: AppStyle.title.copyWith(fontWeight: FontWeight.w600),
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// if (fullAddress.isNotEmpty) ...[
// const SizedBox(height: 3),
// Text(
// fullAddress,
// style: AppStyle.subtitle
// .copyWith(fontSize: 13, color: AppColor.greyColor),
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// ),
// ]
// ],
// ),
// ),
// ],
// ),
// ),
// );
// }
// // // --- منطق التعامل مع الطلبات (Logic Handlers) ---
// // Future<void> _handleAcceptOrder(OrderRequestController controller) async {
// // // 1. محاولة تحديث الحالة في السيرفر
// // // 1. إظهار لودينج وإيقاف التفاعل
// // Get.dialog(const Center(child: CircularProgressIndicator()),
// // barrierDismissible: false);
// // // هذا الرابط يجب أن يكون لملف PHP الآمن الذي يحتوي على rowCount
// // var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: {
// // 'id': (controller.myList[16]),
// // 'rideTimeStart': DateTime.now().toString(),
// // 'status': 'Apply',
// // 'driver_id': box.read(BoxName.driverID),
// // });
// // Log.print('oreder response update: ${res}');
// // // 2. إغلاق اللودينج بمجرد وصول الرد
// // Get.back(); // إغلاق اللودينج
// // // 2. معالجة الفشل (Robust Error Handling)
// // // نفحص إذا كانت النتيجة فشل سواء وصلت كنص أو كـ JSON
// // bool isFailed = false;
// // if (res == 'failure') isFailed = true;
// // if (res is Map && res['status'] == 'failure') isFailed = true;
// // if (isFailed) {
// // MyDialog().getDialog(
// // "This ride is already applied by another driver.".tr, '', () {
// // Get.back(); // يغلق نافذة التنبيه (Dialog)
// // Get.back(); // يغلق صفحة الطلب بالكامل (Screen) ويرجع للخريطة
// // });
// // return; // توقف تام للكود هنا، لن يتم تنفيذ أي سطر بالأسفل
// // }
// // // 3. معالجة النجاح (Success Handling)
// // // إيقاف المؤقت وتحديث الواجهة
// // controller.endTimer();
// // controller.changeApplied();
// // // تحديث حالة السائق في التطبيق
// // Get.put(HomeCaptainController()).changeRideId();
// // box.write(BoxName.statusDriverLocation, 'on');
// // // *هام*: تم حذف استدعاء الـ API الثاني المكرر هنا لأنه غير ضروري وقد يسبب مشاكل
// // // تسجيل الطلب في سجل السائقين (Driver History)
// // 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,
// // tone: 'start',
// // driverList: bodyToPassenger,
// // category: 'Accepted Ride',
// // );
// // // حفظ البيانات في الصندوق (Box) للانتقال للصفحة التالية
// // 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.back(); // يغلق صفحة الطلب الحالية
// // Get.to(() => PassengerLocationMapPage(),
// // arguments: box.read(BoxName.rideArguments));
// // Log.print(
// // 'box.read(BoxName.rideArguments): ${box.read(BoxName.rideArguments)}');
// // }
// // void _handleRefuseOrder(OrderRequestController controller, String rideId) {
// controller.endTimer();
// // controller.refuseOrder(rideId);
// // تسجيل الرفض في الإشعارات المحلية للسائق
// controller.addRideToNotificationDriverString(
// rideId,
// _getData(29),
// _getData(30),
// '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}',
// '${DateTime.now().hour}:${DateTime.now().minute}',
// _getData(2),
// _getData(7),
// 'wait',
// _getData(31),
// _getData(33),
// _getData(2),
// _getData(5),
// _getData(4));
// // الخروج من الصفحة بعد الرفض
// Get.back();
// }
// // }
@@ -0,0 +1,230 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:math' as math;
import '../../../../constant/colors.dart';
import '../../../../controller/home/captin/home_captain_controller.dart';
class OrderRequestPageTest extends StatefulWidget {
const OrderRequestPageTest({super.key});
@override
State<OrderRequestPageTest> createState() => _OrderRequestPageTestState();
}
class _OrderRequestPageTestState extends State<OrderRequestPageTest> {
late OrderRequestController _orderRequestController;
@override
void initState() {
super.initState();
// Initialize the controller and process arguments
_initializeController();
}
void _initializeController() {
// Get the controller or create a new one if not exists
_orderRequestController = Get.put(OrderRequestController());
// Process arguments passed to the page
final arguments = Get.arguments;
final myListString = arguments['myListString'];
var myList =
arguments['DriverList'] == null || arguments['DriverList'].isEmpty
? jsonDecode(myListString)
: arguments['DriverList'];
// Parse coordinates and prepare map data
_orderRequestController.parseCoordinates(myList);
// Start timer and calculate fuel consumption
_orderRequestController.startTimer(
myList[6].toString(),
myList[16].toString(),
);
_orderRequestController.calculateConsumptionFuel();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Container(
color: const Color.fromARGB(255, 241, 238, 238),
child: ListView(
children: [
SizedBox(
height: Get.height * .33,
child: Obx(() => GoogleMap(
initialCameraPosition: CameraPosition(
zoom: 12,
target:
Get.find<HomeCaptainController>().myLocation,
),
cameraTargetBounds: CameraTargetBounds(
_orderRequestController.mapBounds.value),
myLocationButtonEnabled: true,
trafficEnabled: false,
buildingsEnabled: false,
mapToolbarEnabled: true,
myLocationEnabled: true,
markers: _orderRequestController.markers.value,
polylines: _orderRequestController.polylines.value,
onMapCreated: (GoogleMapController controller) {
_orderRequestController.mapController.value =
controller;
},
onCameraMove: (CameraPosition position) {
_orderRequestController
.updateCameraPosition(position);
},
)),
),
// Rest of your UI components
],
),
),
),
],
),
);
}
}
class OrderRequestController extends GetxController {
// Reactive variables for map-related data
Rx<LatLngBounds> mapBounds = Rx<LatLngBounds>(LatLngBounds(
southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)));
Rx<Set<Marker>> markers = Rx<Set<Marker>>({});
Rx<Set<Polyline>> polylines = Rx<Set<Polyline>>({});
Rx<GoogleMapController?> mapController = Rx<GoogleMapController?>(null);
// Icons for start and end markers
late BitmapDescriptor startIcon;
late BitmapDescriptor endIcon;
// Coordinates for passenger location and destination
Rx<LatLng?> passengerLocation = Rx<LatLng?>(null);
Rx<LatLng?> passengerDestination = Rx<LatLng?>(null);
@override
void onInit() {
super.onInit();
// Initialize marker icons
_initializeMarkerIcons();
}
void _initializeMarkerIcons() async {
// Load custom marker icons
startIcon = await BitmapDescriptor.fromAssetImage(
const ImageConfiguration(size: Size(48, 48)),
'assets/start_marker.png');
endIcon = await BitmapDescriptor.fromAssetImage(
const ImageConfiguration(size: Size(48, 48)), 'assets/end_marker.png');
}
void parseCoordinates(List myList) {
// Parse coordinates from the input list
var cords = myList[0].split(',');
var cordDestination = myList[1].split(',');
double latPassengerLocation = double.parse(cords[0]);
double lngPassengerLocation = double.parse(cords[1]);
double latPassengerDestination = double.parse(cordDestination[0]);
double lngPassengerDestination = double.parse(cordDestination[1]);
// Update passenger location and destination
passengerLocation.value =
LatLng(latPassengerLocation, lngPassengerLocation);
passengerDestination.value =
LatLng(latPassengerDestination, lngPassengerDestination);
// Create markers
_createMarkers();
// Create polyline
_createPolyline();
// Calculate map bounds
_calculateMapBounds();
}
void _createMarkers() {
if (passengerLocation.value == null || passengerDestination.value == null)
return;
markers.value = {
Marker(
markerId: MarkerId('MyLocation'.tr),
position: passengerLocation.value!,
draggable: true,
icon: startIcon,
),
Marker(
markerId: MarkerId('Destination'.tr),
position: passengerDestination.value!,
draggable: true,
icon: endIcon,
),
};
}
void _createPolyline() {
if (passengerLocation.value == null || passengerDestination.value == null)
return;
polylines.value = {
Polyline(
zIndex: 1,
consumeTapEvents: true,
geodesic: true,
endCap: Cap.buttCap,
startCap: Cap.buttCap,
visible: true,
polylineId: const PolylineId('routeOrder'),
points: [passengerLocation.value!, passengerDestination.value!],
color: AppColor.primaryColor,
width: 2,
),
};
}
void _calculateMapBounds() {
if (passengerLocation.value == null || passengerDestination.value == null)
return;
double minLatitude = math.min(passengerLocation.value!.latitude,
passengerDestination.value!.latitude);
double maxLatitude = math.max(passengerLocation.value!.latitude,
passengerDestination.value!.latitude);
double minLongitude = math.min(passengerLocation.value!.longitude,
passengerDestination.value!.longitude);
double maxLongitude = math.max(passengerLocation.value!.longitude,
passengerDestination.value!.longitude);
mapBounds.value = LatLngBounds(
southwest: LatLng(minLatitude, minLongitude),
northeast: LatLng(maxLatitude, maxLongitude),
);
}
void updateCameraPosition(CameraPosition position) {
// Implement any specific logic for camera position updates
}
void startTimer(String param1, String param2) {
// Implement timer start logic
}
void calculateConsumptionFuel() {
// Implement fuel consumption calculation
}
}
@@ -0,0 +1,237 @@
import 'dart:convert';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../main.dart';
import '../../../../print.dart';
class VipOrderPage extends StatelessWidget {
const VipOrderPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(VipOrderController());
return MyScafolld(
title: 'VIP Order'.tr,
body: [
GetBuilder<VipOrderController>(builder: (vipOrderController) {
if (vipOrderController.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (vipOrderController.tripData.isEmpty) {
return Center(
child: Text('No orders available'.tr),
);
}
final order = vipOrderController.tripData[0];
Log.print('order: ${order}');
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Passenger Section
_buildSectionTitle('Passenger Information'.tr),
_buildInfoCard(
children: [
_buildDetailRow('Name'.tr,
'${order['passengerName'] ?? 'Unknown'} ${order['passengerLastName'] ?? ''}'),
_buildDetailRow(
'Phone'.tr, order['passengerPhone'] ?? 'Unknown'),
_buildDetailRow(
'Gender'.tr, order['passengergender'] ?? 'Unknown'),
_buildDetailRow('time Selected'.tr,
order['timeSelected'] ?? 'Unknown'),
_buildDetailRow(
'Ride Status'.tr, order['status'] ?? 'Unknown'),
// _buildDetailRow('Ride Status'.tr,
// vipOrderController.myList[4] ?? 'Unknown'),
IconButton(
onPressed: () {
// print(vipOrderController.myList);
},
icon: const Icon(Icons.add),
),
],
),
const SizedBox(height: 24),
// Action Buttons
_buildActionButtons(context),
],
),
),
);
})
],
isleading: true,
);
}
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
);
}
Widget _buildInfoCard({required List<Widget> children}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
Expanded(
flex: 3,
child: Text(
value,
style: const TextStyle(color: Colors.black54),
),
),
],
),
);
}
Widget _buildActionButtons(BuildContext context) {
return Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => _onReject(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Reject'.tr,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => _onApply(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Apply'.tr,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
void _onReject(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ride Rejected'),
backgroundColor: Colors.red,
),
);
}
void _onApply(BuildContext context) {
// TODO: Implement application logic
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ride Applied'),
backgroundColor: Colors.green,
),
);
}
class VipOrderController extends GetxController {
bool isLoading = false;
List tripData = [];
fetchOrder() async {
isLoading = true; // Set loading state
update(); // Notify listeners
var res = await CRUD().get(link: AppLink.getMishwariDriver, payload: {
'driverId': box.read(BoxName.driverID).toString(),
});
if (res != 'failure') {
tripData = jsonDecode(res)['message'];
} else {
tripData = [];
}
isLoading = false; // Loading complete
update(); // Notify listeners
}
@override
void onInit() async {
fetchOrder();
super.onInit();
}
}
+46
View File
@@ -0,0 +1,46 @@
// import 'dart:io';
//
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import '../../../controller/functions/ocr_controller.dart';
//
// class PassportDataExtractorWidget extends StatelessWidget {
// final PassportDataExtractor passportDataExtractor =
// Get.put(PassportDataExtractor());
// final PassportDataController controller = Get.put(PassportDataController());
//
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// title: const Text('Passport Data Extractor'),
// ),
// body: Column(
// children: [
// ElevatedButton(
// onPressed: controller.extractDataAndDrawBoundingBoxes,
// child: const Text('Extract Data'),
// ),
// Expanded(
// child: Center(
// child: Stack(
// children: [
// GetBuilder<PassportDataController>(
// builder: (controller) => CustomPaint(
// painter: BoundingBoxPainter(
// controller.extractedTextWithCoordinates),
// child: GetBuilder<PassportDataExtractor>(
// builder: (controller) =>
// Image.file(File(passportDataExtractor.image!.path)),
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// ],
// ),
// );
// }
// }
+175
View File
@@ -0,0 +1,175 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../controller/functions/ocr_controller.dart';
class TextRecognizerAPI extends StatelessWidget {
const TextRecognizerAPI({super.key});
@override
Widget build(BuildContext context) {
Get.put(ScanDocumentsByApi());
return GetBuilder<ScanDocumentsByApi>(
builder: (controller) => Scaffold(
appBar: AppBar(
title: Text('Api'),
actions: [
IconButton(
onPressed: () {
controller.matchFaceApi();
},
icon: const Icon(Icons.face),
),
],
),
body: Center(
child: controller.isLoading
? const MyCircularProgressIndicator()
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.memory(
controller.imagePortrait,
width: 60,
),
Image.memory(
controller.imageSignature,
width: 60,
),
Image.memory(
controller.imageDocumentFrontSide,
width: 250,
),
],
),
Text(controller.responseMap['authenticity_meta']
.toString()),
Text(
'countryName: ${controller.responseMap['data']['countryName'].toString()}'),
Text(
'Expiry Date: ${controller.responseMap['data']['ocr']['dateOfExpiry'].toString()}'),
// Add more Text widgets to display other record values
Text(
'Address: ${controller.responseMap['data']['ocr']['address'].toString()}'),
Text(
'City: ${controller.responseMap['data']['ocr']['addressCity'].toString()}'),
Text(
'Jurisdiction Code: ${controller.responseMap['data']['ocr']['addressJurisdictionCode'].toString()}'),
Text(
'Postal Code: ${controller.responseMap['data']['ocr']['addressPostalCode'].toString()}'),
Text(
'Street: ${controller.responseMap['data']['ocr']['addressStreet'].toString()}'),
Text(
'Date of Birth: ${controller.responseMap['data']['ocr']['dateOfBirth'].toString()}'),
Text(
'Date of Issue: ${controller.responseMap['data']['ocr']['dateOfIssue'].toString()}'),
Text(
'Drivers License Class: ${controller.responseMap['data']['ocr']['dlClass'].toString()}'),
Text(
'Document Number: ${controller.responseMap['data']['ocr']['documentNumber'].toString()}'),
Text(
'Eye Color: ${controller.responseMap['data']['ocr']['eyesColor'].toString()}'),
Text(
'Given Names: ${controller.responseMap['data']['ocr']['givenNames'].toString()}'),
Text(
'Height: ${controller.responseMap['data']['ocr']['height'].toString()}'),
Text(
'Issuing State Code: ${controller.responseMap['data']['ocr']['issuingStateCode'].toString()}'),
Text(
'Name: ${controller.responseMap['data']['ocr']['name'].toString()}'),
Text(
'Sex: ${controller.responseMap['data']['ocr']['sex'].toString()}'),
Text(
'Surname: ${controller.responseMap['data']['ocr']['surname'].toString()}'),
Text(
'Valid State: ${controller.responseMap['data']['ocr']['validState'].toString()}'),
],
),
))));
}
}
// class TextExtractionView extends StatelessWidget {
// TextExtractionView({super.key});
//
// @override
// Widget build(BuildContext context) {
// Get.put(TextExtractionController());
// return Scaffold(
// appBar: AppBar(
// title: const Text('Text Extraction'),
// ),
// body: GetBuilder<TextExtractionController>(builder: (controller) {
// return Center(
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// ElevatedButton(
// onPressed: () {},
// child: const Text('Pick Image and Extract Text'),
// ),
// const SizedBox(height: 20),
// controller.isloading
// ? const MyCircularProgressIndicator()
// : Text(controller.extractedText),
// ],
// ),
// );
// }),
// );
// }
// }
// class TextRecognizerWidget extends StatelessWidget {
// const TextRecognizerWidget({super.key});
//
// @override
// Widget build(BuildContext context) {
// Get.put(TextMLGoogleRecognizerController());
// return GetBuilder<TextMLGoogleRecognizerController>(
// builder: (controller) => Scaffold(
// appBar: AppBar(),
// body: Center(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text('${controller.decode['DRIVER_LICENSE'].toString()}'),
// Text('DL: ${controller.decode['dl_number'].toString()}'),
// Text(
// 'Expiry Date: ${controller.decode['expiry_date'].toString()}'),
// Text('Last Name: ${controller.decode['lastName'].toString()}'),
// Text(
// 'First Name: ${controller.decode['firstName'].toString()}'),
// Text('Address: ${controller.decode['address'].toString()}'),
// Text('Date of Birth: ${controller.decode['dob'].toString()}'),
// Text('RSTR: ${controller.decode['rstr'].toString()}'),
// Text('Class: ${controller.decode['class'].toString()}'),
// Text('End: ${controller.decode['end'].toString()}'),
// Text('DD: ${controller.decode['dd'].toString()}'),
// Text('Sex: ${controller.decode['sex'].toString()}'),
// Text('Hair: ${controller.decode['hair'].toString()}'),
// Text('Eyes: ${controller.decode['eyes'].toString()}'),
// // and so on for other fields
// ],
// ))));
// }
// }
// class PassportPage extends StatelessWidget {
// PassportPage({super.key});
// PassportRecognizerController passportRecognizerController =
// Get.put(PassportRecognizerController());
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// title: const Text('Driver License'),
// ),
// body: const Center(child: Text('data')));
// }
// }
@@ -0,0 +1,140 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class BankController extends GetxController {
String selectedBank = '';
Map<String, String> bankNames = {
'Ahli United Bank'.tr: 'AUB',
'Citi Bank N.A. Egypt'.tr: 'CITI',
'MIDBANK'.tr: 'MIDB',
'Banque Du Caire'.tr: 'BDC',
'HSBC Bank Egypt S.A.E'.tr: 'HSBC',
'Credit Agricole Egypt S.A.E'.tr: 'ECAE',
'Egyptian Gulf Bank'.tr: 'EGB',
'The United Bank'.tr: 'UB',
'Qatar National Bank Alahli'.tr: 'QNB',
'Arab Bank PLC'.tr: 'ARAB',
'Emirates National Bank of Dubai'.tr: 'ENBD',
'Al Ahli Bank of Kuwait – Egypt'.tr: 'ABK',
'National Bank of Kuwait – Egypt'.tr: 'NBK',
'Arab Banking Corporation - Egypt S.A.E'.tr: 'EABC',
'First Abu Dhabi Bank'.tr: 'FAB',
'Abu Dhabi Islamic Bank – Egypt'.tr: 'ADIB',
'Commercial International Bank - Egypt S.A.E'.tr: 'CIB',
'Housing And Development Bank'.tr: 'HDB',
'Banque Misr'.tr: 'MISR',
'Arab African International Bank'.tr: 'AAIB',
'Egyptian Arab Land Bank'.tr: 'EALB',
'Export Development Bank of Egypt'.tr: 'EDBE',
'Faisal Islamic Bank of Egypt'.tr: 'FAIB',
'Blom Bank'.tr: 'BLOM',
'Abu Dhabi Commercial Bank – Egypt'.tr: 'ADCB',
'Alex Bank Egypt'.tr: 'BOA',
'Societe Arabe Internationale De Banque'.tr: 'SAIB',
'National Bank of Egypt'.tr: 'NBE',
'Al Baraka Bank Egypt B.S.C.'.tr: 'ABRK',
'Egypt Post'.tr: 'POST',
'Nasser Social Bank'.tr: 'NSB',
'Industrial Development Bank'.tr: 'IDB',
'Suez Canal Bank'.tr: 'SCB',
'Mashreq Bank'.tr: 'MASHA',
'Arab Investment Bank'.tr: 'AIB',
'General Authority For Supply Commodities'.tr: 'GASCA',
'Arab International Bank'.tr: 'AIB',
'Agricultural Bank of Egypt'.tr: 'PDAC',
'National Bank of Greece'.tr: 'NBG',
'Central Bank Of Egypt'.tr: 'CBE',
'ATTIJARIWAFA BANK Egypt'.tr: 'BBE',
};
@override
void onInit() {
super.onInit();
selectedBank = bankNames.values.first;
}
void updateSelectedBank(String? bankShortName) {
selectedBank = bankShortName ?? '';
update();
}
List<DropdownMenuItem<String>> getDropdownItems() {
return bankNames.keys.map<DropdownMenuItem<String>>((bankFullName) {
return DropdownMenuItem<String>(
value: bankNames[bankFullName],
child: Text(bankFullName),
);
}).toList();
}
void showBankPicker(BuildContext context) {
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) => CupertinoActionSheet(
title: Text('Select a Bank'.tr),
actions: bankNames.keys.map((String bankFullName) {
return CupertinoActionSheetAction(
child: Text(bankFullName),
onPressed: () {
updateSelectedBank(bankNames[bankFullName]);
Navigator.pop(context);
},
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
child: Text('Cancel'.tr),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
class BankDropdown extends StatelessWidget {
final BankController bankController = Get.put(BankController());
@override
Widget build(BuildContext context) {
return GetBuilder<BankController>(
init: bankController,
builder: (controller) {
return CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => controller.showBankPicker(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: CupertinoColors.systemGrey4),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
controller.selectedBank != null
? controller.bankNames.keys.firstWhere(
(key) =>
controller.bankNames[key] ==
controller.selectedBank,
orElse: () => 'Select a Bank'.tr,
)
: 'Select a Bank'.tr,
style: TextStyle(
color: controller.selectedBank != null
? CupertinoColors.black
: CupertinoColors.systemGrey,
),
),
const Icon(CupertinoIcons.chevron_down, size: 20),
],
),
),
);
},
);
}
}
@@ -0,0 +1,410 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../controller/home/payment/captain_wallet_controller.dart';
import '../../../controller/home/payment/paymob_payout.dart';
import '../../../main.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
class CardTripzWalletDriver extends StatelessWidget {
const CardTripzWalletDriver({super.key});
@override
Widget build(BuildContext context) {
// التأكد من تهيئة الكنترولر
Get.put(PaymobPayout());
return Center(
child: GetBuilder<CaptainWalletController>(
builder: (controller) {
// جلب الرصيد
double balance = double.tryParse(controller.totalAmountVisa) ?? 0.0;
return Container(
width: double.infinity,
height: 220,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient(
colors: [
Color(0xFF1A2980),
Color(0xFF26D0CE)
], // تدرج أزرق احترافي
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF1A2980).withOpacity(0.4),
blurRadius: 15,
offset: const Offset(0, 10),
),
],
),
child: Stack(
children: [
// خلفية زخرفية
Positioned(
right: -30,
top: -30,
child: Icon(Icons.account_balance_wallet,
size: 150, color: Colors.white.withOpacity(0.1)),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// الهيدر
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"محفظة تريبز", // Tripz Wallet
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
const SizedBox(height: 4),
const Text(
"رصيد الكابتن",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: const Row(
children: [
Icon(Icons.verified_user,
color: Colors.white, size: 16),
SizedBox(width: 5),
Text("مؤمنة",
style: TextStyle(
color: Colors.white, fontSize: 12)),
],
),
)
],
),
// الرصيد
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
controller.totalAmountVisa,
style: const TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
const SizedBox(width: 5),
Text(
"ج.م",
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
),
],
),
// زر السحب واسم السائق
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
box.read(BoxName.nameDriver) ?? "الكابتن",
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
),
),
// زر طلب السحب
InkWell(
onTap: () =>
_handleCashOutRequest(context, balance),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 5,
offset: const Offset(0, 2),
)
],
),
child: Row(
children: [
Text(
"سحب الأرباح", // Cash Out
style: TextStyle(
color: const Color(0xFF1A2980),
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded,
size: 18, color: const Color(0xFF1A2980)),
],
),
),
),
],
),
],
),
),
],
),
);
},
),
);
}
// --- Logic for Cash Out ---
void _handleCashOutRequest(BuildContext context, double balance) {
// 1. التحقق من الحد الأدنى (مصر = 100، غير مصر = 20)
double minAmount = box.read(BoxName.countryCode) != 'Egypt' ? 100.0 : 20.0;
if (balance < minAmount) {
_showErrorDialog(context, "رصيدك غير كافي",
"عشان تقدر تسحب، لازم رصيدك يكون معدي $minAmount جنيه.\nرصيدك الحالي: $balance جنيه");
return;
}
// 2. فتح نافذة السحب الآمنة
_showSecurePayoutSheet(context, balance);
}
void _showSecurePayoutSheet(BuildContext context, double balance) {
final controller = Get.find<CaptainWalletController>();
final payoutController = Get.find<PaymobPayout>();
// تنظيف الحقل
controller.phoneWallet.clear();
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// رأس النافذة
Center(
child: Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10)),
),
),
const SizedBox(height: 20),
const Text("طلب سحب رصيد",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Text("سيتم تحويل المبلغ لمحفظتك الإلكترونية",
style: TextStyle(color: Colors.grey[600], fontSize: 12)),
const SizedBox(height: 20),
// اختيار المحفظة
const Text("نوع المحفظة",
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildWalletDropdown(payoutController),
const SizedBox(height: 15),
// إدخال الرقم
Form(
key: controller.formKey,
child: MyTextForm(
controller: controller.phoneWallet,
label: "رقم المحفظة (فودافون/اتصالات..)",
hint: "01xxxxxxxxx",
type: TextInputType.phone,
),
),
const SizedBox(height: 20),
// تفاصيل العملية (الفاتورة)
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
children: [
_buildSummaryRow("مبلغ السحب", "$balance ج.م"),
_buildSummaryRow("مصاريف التحويل", "5.0 ج.م",
isDeduction: true),
const Divider(),
_buildSummaryRow("الصافي ليك", "${balance - 5} ج.م",
isTotal: true),
],
),
),
const SizedBox(height: 20),
// زر التأكيد
SizedBox(
width: double.infinity,
child: MyElevatedButton(
title: "تأكيد السحب",
kolor: const Color(0xFF1A2980),
onPressed: () async {
if (controller.formKey.currentState!.validate()) {
// إغلاق الشيت
Get.back();
// تنفيذ العملية
String amountAfterFees = (balance - 5).toStringAsFixed(0);
await payoutController.payToWalletDriverAll(
amountAfterFees,
payoutController.dropdownValue.toString(),
controller.phoneWallet.text.toString(),
);
}
},
),
),
const SizedBox(height: 10),
],
),
),
),
isScrollControlled: true,
);
}
// --- Helper Widgets ---
Widget _buildWalletDropdown(PaymobPayout controller) {
return GetBuilder<PaymobPayout>(
builder: (_) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(10),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: controller.dropdownValue,
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down_rounded,
color: Color(0xFF1A2980)),
items: <String>['etisalat', 'aman', 'orange', 'vodafone']
.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Row(
children: [
const Icon(Icons.account_balance_wallet_outlined,
size: 18, color: Colors.grey),
const SizedBox(width: 10),
Text(value,
style: const TextStyle(fontWeight: FontWeight.w500)),
],
),
);
}).toList(),
onChanged: (newValue) {
controller.dropdownValue = newValue!;
controller.update();
},
),
),
);
},
);
}
Widget _buildSummaryRow(String title, String value,
{bool isDeduction = false, bool isTotal = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title,
style: TextStyle(
color: isTotal ? Colors.black : Colors.grey[600],
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
fontSize: isTotal ? 16 : 14,
)),
Text(value,
style: TextStyle(
color: isDeduction
? Colors.red
: (isTotal ? const Color(0xFF1A2980) : Colors.black),
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
fontSize: isTotal ? 18 : 14,
)),
],
),
);
}
void _showErrorDialog(BuildContext context, String title, String message) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(title, style: const TextStyle(color: Colors.red)),
content: Text(message),
actions: [
CupertinoDialogAction(
child: const Text("تمام"),
onPressed: () => Navigator.pop(context),
)
],
),
);
}
}
@@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
// --- ملاحظات هامة ---
// 1. تأكد من إضافة الرابط الجديد إلى ملف AppLink الخاص بك:
// static const String payWithEcashDriver = "$server/payment/payWithEcashDriver.php";
//
// 2. تأكد من أنك تخزن 'driverId' في الـ box الخاص بك، مثلاً:
// box.read(BoxName.driverID)
/// دالة جديدة لبدء عملية الدفع للسائق عبر ecash
Future<void> payWithEcashDriver(BuildContext context, String amount) async {
try {
// يمكنك استخدام نفس طريقة التحقق بالبصمة إذا أردت
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
);
if (didAuthenticate) {
// استدعاء الـ Endpoint الجديد على السيرفر الخاص بك
var res = await CRUD().postWallet(
link: AppLink.payWithEcashDriver,
payload: {
// أرسل البيانات التي يحتاجها السيرفر
"amount": amount,
// "driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
"driverId": box.read(BoxName.driverID), // تأكد من وجود هذا المتغير
},
);
// التأكد من أن السيرفر أعاد رابط الدفع بنجاح
if (res != null &&
res['status'] == 'success' &&
res['message'] != null) {
final String paymentUrl = res['message'];
// الانتقال إلى شاشة الدفع الجديدة الخاصة بـ ecash للسائق
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EcashDriverPaymentScreen(paymentUrl: paymentUrl),
),
);
} else {
// عرض رسالة خطأ في حال فشل السيرفر في إنشاء الرابط
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'Failed to initiate payment. Please try again.'.tr,
style: AppStyle.title,
),
);
}
}
}
} catch (e) {
Get.defaultDialog(
title: 'Error'.tr,
content: Text(
'An error occurred during the payment process.'.tr,
style: AppStyle.title,
),
);
}
}
/// شاشة جديدة ومبسطة خاصة بدفع السائقين عبر ecash
class EcashDriverPaymentScreen extends StatefulWidget {
final String paymentUrl;
const EcashDriverPaymentScreen({required this.paymentUrl, Key? key})
: super(key: key);
@override
State<EcashDriverPaymentScreen> createState() =>
_EcashDriverPaymentScreenState();
}
class _EcashDriverPaymentScreenState extends State<EcashDriverPaymentScreen> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageFinished: (url) {
print('Ecash Driver WebView URL Finished: $url');
// هنا نتحقق فقط من أن المستخدم عاد إلى صفحة النجاح
// لا حاجة لاستدعاء أي API هنا، فالـ Webhook يقوم بكل العمل
if (url.contains("success.php")) {
showProcessingDialog();
}
},
))
..loadRequest(Uri.parse(widget.paymentUrl));
}
// دالة لعرض رسالة "العملية قيد المعالجة"
void showProcessingDialog() {
showDialog(
barrierDismissible: false,
context: Get.context!,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
Text(
"Payment Successful".tr,
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
content: Text(
"Your payment is being processed and your wallet will be updated shortly."
.tr,
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () {
// أغلق مربع الحوار، ثم أغلق شاشة الدفع
Navigator.pop(context); // Close the dialog
Navigator.pop(context); // Close the payment screen
},
style: TextButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
child: Text(
"OK".tr,
style: const TextStyle(color: Colors.white),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Complete Payment'.tr)),
body: WebViewWidget(controller: _controller),
);
}
}
@@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
import '../../../controller/payment/smsPaymnet/pay_out_syria_controller.dart';
class PayoutScreen extends StatefulWidget {
// استقبال كل البيانات المطلوبة جاهزة
final double amountToWithdraw;
final String payoutPhoneNumber;
final String walletType;
const PayoutScreen({
super.key,
required this.amountToWithdraw,
required this.payoutPhoneNumber,
required this.walletType,
});
@override
_PayoutScreenState createState() => _PayoutScreenState();
}
class _PayoutScreenState extends State<PayoutScreen> {
final _payoutService = PayoutService();
final _localAuth = LocalAuthentication();
bool _isLoading = false;
Future<void> _handlePayoutRequest() async {
try {
// 1. طلب المصادقة البيومترية
bool didAuthenticate = await _localAuth.authenticate(
localizedReason: 'استخدم بصمة الإصبع لتأكيد عملية السحب',
options: const AuthenticationOptions(
biometricOnly: true,
sensitiveTransaction: true,
),
);
if (didAuthenticate && mounted) {
setState(() => _isLoading = true);
// 2. إرسال الطلب إلى السيرفر بالبيانات الجاهزة
final result = await _payoutService.requestPayout(
driverId:
box.read(BoxName.driverID).toString(), // استبدله بـ box.read
amount: widget.amountToWithdraw,
payoutPhoneNumber: widget.payoutPhoneNumber,
walletType: widget.walletType,
);
setState(() => _isLoading = false);
if (result != null && result.contains("successfully")) {
// 3. عرض رسالة النجاح النهائية
_showSuccessDialog();
} else {
_showErrorDialog(result ?? "حدث خطأ غير معروف.");
}
}
} catch (e) {
setState(() => _isLoading = false);
_showErrorDialog("جهازك لا يدعم المصادقة البيومترية أو لم يتم إعدادها.");
debugPrint("Biometric error: $e");
}
}
@override
Widget build(BuildContext context) {
// حساب المبلغ الإجمالي المخصوم
final totalDeducted = widget.amountToWithdraw + PayoutService.payoutFee;
return Scaffold(
appBar: AppBar(title: const Text("تأكيد سحب الأموال")),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.wallet, size: 64, color: Colors.blue),
const SizedBox(height: 16),
Text(
"تأكيد تفاصيل عملية السحب",
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
_buildSummaryCard(totalDeducted),
const SizedBox(height: 32),
_isLoading
? const Center(child: CircularProgressIndicator())
: ElevatedButton.icon(
onPressed: _handlePayoutRequest,
icon: const Icon(Icons.fingerprint),
label: const Text("تأكيد السحب بالبصمة"),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
Widget _buildSummaryCard(double totalDeducted) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
_summaryRow("المبلغ المسحوب:",
"${widget.amountToWithdraw.toStringAsFixed(2)} ل.س"),
const Divider(),
_summaryRow("عمولة السحب:",
"${PayoutService.payoutFee.toStringAsFixed(2)} ل.س"),
const Divider(thickness: 1.5),
_summaryRow(
"الإجمالي المخصوم من رصيدك:",
"${totalDeducted.toStringAsFixed(2)} ل.س",
isTotal: true,
),
const SizedBox(height: 16),
_summaryRow("سيتم التحويل إلى هاتف:", widget.payoutPhoneNumber),
_summaryRow("عبر محفظة:", widget.walletType),
],
),
),
);
}
Widget _summaryRow(String title, String value, {bool isTotal = false}) {
final titleStyle = TextStyle(
fontSize: 16,
color: isTotal ? Theme.of(context).primaryColor : Colors.black87,
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
);
final valueStyle = titleStyle.copyWith(
fontWeight: FontWeight.bold,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: titleStyle),
Text(value, style: valueStyle),
],
),
);
}
void _showErrorDialog(String message) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('حدث خطأ'),
content: Text(message),
actions: [
TextButton(
child: const Text('موافق'),
onPressed: () => Navigator.of(ctx).pop())
],
),
);
}
void _showSuccessDialog() {
if (!mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: const Text('تم إرسال طلبك بنجاح'),
content: Text(
"سيتم تحويل المال إلى المحفظة التي أوردتها (${widget.walletType})، إلى الرقم ${widget.payoutPhoneNumber}، خلال مدة قصيرة. يرجى الانتظار، ستصلك رسالة تأكيد من محفظتك حال وصولها. شكراً لك."),
actions: [
TextButton(
child: const Text('موافق'),
onPressed: () {
Navigator.of(ctx).pop();
Navigator.of(context).pop();
},
),
],
),
);
}
}
@@ -0,0 +1,407 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:intl/intl.dart' as intl; // For date formatting
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../controller/payment/driver_payment_controller.dart';
class PaymentHistoryDriverPage extends StatelessWidget {
const PaymentHistoryDriverPage({super.key});
@override
Widget build(BuildContext context) {
// التأكد من حقن الكونترولر
Get.put(DriverWalletHistoryController());
return Scaffold(
backgroundColor: const Color(0xFFF8F9FD), // خلفية هادئة عصرية
appBar: AppBar(
title: Text(
'Financial Wallet'.tr,
style: const TextStyle(
color: Colors.black87, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87),
onPressed: () => Get.back(),
),
actions: [
IconButton(
icon: const Icon(Icons.filter_list_rounded,
color: AppColor.primaryColor),
onPressed: () {
// يمكن إضافة فلترة هنا مستقبلاً
},
)
],
),
body: GetBuilder<DriverWalletHistoryController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
// تجميع البيانات للمخطط
final List<Map<String, dynamic>> chartData =
_processChartData(controller.archive);
return Column(
children: [
// 1. المخطط البياني الأسبوعي (Weekly Chart)
_buildWeeklyChartSection(chartData),
const SizedBox(height: 10),
// 2. قائمة المعاملات (Transactions List)
Expanded(
child: controller.archive.isEmpty
? _buildEmptyState(context)
: _buildTransactionList(controller.archive),
),
],
);
},
),
);
}
// --- قسم المخطط البياني ---
Widget _buildWeeklyChartSection(List<Map<String, dynamic>> data) {
// حساب أعلى قيمة لضبط ارتفاع الأعمدة نسبياً
double maxAmount = 0;
for (var item in data) {
if ((item['amount'] as double) > maxAmount) maxAmount = item['amount'];
}
// تجنب القسمة على صفر
if (maxAmount == 0) maxAmount = 1;
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Weekly Earnings".tr,
style: AppStyle.headTitle2.copyWith(fontSize: 16),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColor.greenColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
"Last 7 Days".tr,
style: const TextStyle(
fontSize: 10,
color: AppColor.greenColor,
fontWeight: FontWeight.bold),
),
)
],
),
const SizedBox(height: 20),
// رسم الأعمدة
SizedBox(
height: 120,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.end,
children: data.map((dayData) {
double heightFactor = (dayData['amount'] as double) / maxAmount;
// التأكد من أن العمود له ارتفاع بسيط حتى لو القيمة صفر للجمالية
if (heightFactor < 0.05) heightFactor = 0.05;
bool isToday = dayData['isToday'] ?? false;
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Tooltip للمبلغ عند الضغط (اختياري)
// Bar
Flexible(
child: Container(
width: 12,
height: 100 * heightFactor,
decoration: BoxDecoration(
color: isToday
? AppColor.primaryColor
: AppColor.primaryColor.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
gradient: isToday
? LinearGradient(
colors: [
AppColor.primaryColor,
AppColor.primaryColor.withOpacity(0.7)
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter)
: null,
),
),
),
const SizedBox(height: 8),
// Label (Sat, Sun...)
Text(
dayData['dayName'],
style: TextStyle(
fontSize: 11,
fontWeight:
isToday ? FontWeight.bold : FontWeight.normal,
color: isToday ? AppColor.primaryColor : Colors.grey,
),
),
],
);
}).toList(),
),
),
],
),
);
}
// --- معالجة البيانات للمخطط ---
List<Map<String, dynamic>> _processChartData(List transactionList) {
// 1. تحضير خريطة لأيام الأسبوع (من السبت إلى الجمعة مثلاً)
// نستخدم آخر 7 أيام ديناميكياً
List<Map<String, dynamic>> weekData = [];
DateTime now = DateTime.now();
// إنشاء هيكل لآخر 7 أيام
for (int i = 6; i >= 0; i--) {
DateTime date = now.subtract(Duration(days: i));
String dayKey =
intl.DateFormat('yyyy-MM-dd').format(date); // مفتاح للمقارنة
String dayName =
intl.DateFormat('E', 'ar').format(date); // اسم اليوم (سبت، أحد..)
// اختصار أسماء الأيام بالعربية إذا لزم الأمر
dayName = dayName
.replaceAll('السبت', 'س')
.replaceAll('الأحد', 'ح')
.replaceAll('الاثنين', 'ن')
.replaceAll('الثلاثاء', 'ث')
.replaceAll('الأربعاء', 'ر')
.replaceAll('الخميس', 'خ')
.replaceAll('الجمعة', 'ج');
weekData.add({
'date': dayKey,
'dayName': dayName,
'amount': 0.0,
'isToday': i == 0,
});
}
// 2. تجميع المبالغ من الترانزكشن
for (var trans in transactionList) {
if (trans['created_at'] != null) {
try {
DateTime transDate = DateTime.parse(trans['created_at']);
String transKey = intl.DateFormat('yyyy-MM-dd').format(transDate);
double amount = double.tryParse(trans['amount'].toString()) ?? 0.0;
// نجمع فقط المبالغ الإيجابية (الدخل) للمخطط، أو يمكن جمع الكل حسب رغبتك
// هنا سأجمع كل الحركات الموجبة (الربح)
if (amount > 0) {
for (var day in weekData) {
if (day['date'] == transKey) {
day['amount'] += amount;
break;
}
}
}
} catch (e) {
// ignore date parse error
}
}
}
return weekData;
}
// --- قائمة المعاملات ---
Widget _buildTransactionList(List transactions) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text(
"Recent Transactions".tr,
style: AppStyle.headTitle2.copyWith(fontSize: 18),
),
),
Expanded(
child: AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: transactions.length,
physics: const BouncingScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
var transaction = transactions[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _TransactionCard(transaction: transaction),
),
),
);
},
),
),
),
],
);
}
// --- حالة القائمة الفارغة ---
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(Icons.receipt_long_rounded,
size: 60, color: Colors.grey[400]),
),
const SizedBox(height: 16),
Text(
'No transactions yet'.tr,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
fontWeight: FontWeight.w500),
),
],
),
);
}
}
// --- بطاقة المعاملة (Modern Card) ---
class _TransactionCard extends StatelessWidget {
final Map<String, dynamic> transaction;
const _TransactionCard({required this.transaction});
@override
Widget build(BuildContext context) {
// 1. معالجة البيانات
final double amount =
double.tryParse(transaction['amount']?.toString() ?? '0') ?? 0;
final bool isCredit = amount >= 0; // موجب = دخل، سالب = خصم
final String dateStr = transaction['created_at'] ?? '';
// تنسيق التاريخ ليكون مقروءاً (مثلاً: Yesterday, 10:30 PM)
String displayDate = dateStr;
try {
DateTime dt = DateTime.parse(dateStr);
displayDate = intl.DateFormat('dd MMM, hh:mm a', 'en').format(dt);
// استخدم 'ar' بدل 'en' إذا أردت التاريخ بالعربي
} catch (_) {}
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.03),
spreadRadius: 1,
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isCredit
? AppColor.greenColor.withOpacity(0.1)
: AppColor.redColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
// أيقونات توضيحية
isCredit ? Icons.add_card_rounded : Icons.money_off_rounded,
color: isCredit ? AppColor.greenColor : AppColor.redColor,
size: 24,
),
),
title: Text(
// نوع العملية (يمكن تحسينه بناءً على payment_method)
isCredit ? 'Trip Earnings'.tr : 'Commission / Deduct'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: Colors.black87,
),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
displayDate,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
// العملة المصرية
'${isCredit ? '+' : ''}${amount.toStringAsFixed(0)} ${'EGP'.tr}',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: isCredit ? AppColor.greenColor : AppColor.redColor,
),
),
if (!isCredit) // تلميح إضافي للمصروفات
Text(
"Paid".tr,
style: TextStyle(fontSize: 10, color: Colors.grey[400]),
)
],
),
),
);
}
}
+606
View File
@@ -0,0 +1,606 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:sefer_driver/controller/payment/payment_controller.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
import '../../widgets/my_textField.dart';
class PointsCaptain extends StatefulWidget {
final Color kolor;
final String countPoint;
final double pricePoint;
const PointsCaptain({
super.key,
required this.kolor,
required this.countPoint,
required this.pricePoint,
});
@override
State<PointsCaptain> createState() => _PointsCaptainState();
}
class _PointsCaptainState extends State<PointsCaptain> {
final PaymentController paymentController = Get.put(PaymentController());
final CaptainWalletController captainWalletController =
Get.put(CaptainWalletController());
// متغيرات لحالة التحميل العامة
bool isCardLoading = false;
bool isWalletLoading = false;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () async {
// تم التعديل: إزالة شرط الدولة وتشغيل قائمة Paymob للجميع
// لأنها تدعم البطاقات الدولية الآن
_showPaymentBottomSheet(context);
},
child: Container(
width: Get.width * .28,
height: Get.width * .28,
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
widget.kolor.withOpacity(0.4),
widget.kolor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: widget.kolor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(2, 4),
)
],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withOpacity(0.5), width: 1),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.stars_rounded, color: Colors.white, size: 28),
const SizedBox(height: 5),
Text(
'${widget.countPoint} ${"points".tr}',
style: AppStyle.subtitle.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Container(
margin: const EdgeInsets.only(top: 5),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${widget.pricePoint} ${box.read(BoxName.countryCode) == 'Jordan' ? 'JOD'.tr : 'L.E'.tr}',
style: AppStyle.title.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
],
),
),
);
}
void _showPaymentBottomSheet(BuildContext context) {
// تصفير الحالات عند فتح القائمة
isCardLoading = false;
isWalletLoading = false;
// التحقق مما إذا كان المستخدم في مصر (لعرض خيار المحفظة)
bool isEgypt = box.read(BoxName.countryCode) == 'Egypt';
Get.bottomSheet(
StatefulBuilder(builder: (context, setStateSheet) {
return 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: [
Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
),
const SizedBox(height: 20),
Text(
'Choose Payment Method'.tr,
style: AppStyle.headTitle2,
),
const SizedBox(height: 10),
Text(
'${'You will pay'.tr}: ${widget.pricePoint} ${isEgypt ? 'LE'.tr : 'currency_unit'.tr}', // تحسين بسيط للعملة
style: AppStyle.title.copyWith(color: Colors.grey),
),
const SizedBox(height: 25),
// --- إضافة تنبيه بخصوص البطاقات الدولية ---
Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: AppColor.primaryColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(Icons.public, color: AppColor.primaryColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Global Payment".tr,
style: TextStyle(
color: AppColor.primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
"We accept all international Credit/Debit cards."
.tr,
style: TextStyle(
color: AppColor.primaryColor.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
],
),
),
// --- زر الدفع بالبطاقة ---
_buildPaymentButton(
title: 'Pay with Credit Card'.tr,
icon: Icons.credit_card,
color: AppColor.primaryColor,
isLoading: isCardLoading,
onTap: () async {
setStateSheet(() => isCardLoading = true);
try {
var res = await CRUD().postWallet(
link: AppLink.payWithPayMobCardDriver,
payload: _getPayload(),
);
if (res != null && res['message'] != null) {
Get.back(); // إغلاق القائمة
Get.to(() => GenericPaymentWebView(
iframeUrl: res['message'],
countPrice: widget.countPoint,
isWallet: false,
));
} else {
setStateSheet(() => isCardLoading = false);
mySnackeBarError("Failed to get payment link".tr);
}
} catch (e) {
setStateSheet(() => isCardLoading = false);
mySnackeBarError("Failed to initialize card payment".tr);
}
},
),
// عرض خيار المحفظة فقط إذا كان المستخدم في مصر
if (isEgypt) ...[
const SizedBox(height: 15),
_buildPaymentButton(
title: 'Pay with Wallet'.tr,
icon: Icons.account_balance_wallet,
color: const Color(0xffE60000),
isLoading: isWalletLoading,
onTap: () {
Get.back();
_showWalletNumberDialog();
},
),
],
const SizedBox(height: 20),
],
),
);
}),
isScrollControlled: true,
);
}
Map<String, dynamic> _getPayload() {
return {
"amount": widget.pricePoint.toStringAsFixed(2),
"email": box.read(BoxName.emailDriver).toString(),
"first_name": box.read(BoxName.nameDriver).toString().split(' ')[0],
"last_name": box.read(BoxName.nameDriver).toString().split(' ').length > 1
? box.read(BoxName.nameDriver).toString().split(' ')[1]
: "Driver",
"phone_number": box.read(BoxName.phoneDriver),
};
}
Widget _buildPaymentButton({
required String title,
required IconData icon,
required Color color,
required bool isLoading,
required VoidCallback onTap,
}) {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
onPressed: isLoading ? null : onTap,
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: Colors.white),
const SizedBox(width: 10),
Text(title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold)),
],
),
),
);
}
void _showWalletNumberDialog() {
bool isDialogLoading = false;
Get.defaultDialog(
barrierDismissible: false,
title: 'E-Wallet Number'.tr,
contentPadding: const EdgeInsets.all(20),
content: StatefulBuilder(builder: (context, setStateDialog) {
return Column(
children: [
Form(
key: paymentController.formKey,
child: MyTextForm(
controller: paymentController.walletphoneController,
label: 'Phone Number'.tr,
hint: '01xxxxxxxxx',
type: TextInputType.phone,
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: isDialogLoading
? null
: () async {
if (paymentController.formKey.currentState!
.validate()) {
setStateDialog(() => isDialogLoading = true);
box.write(BoxName.phoneWallet,
paymentController.walletphoneController.text);
try {
var payload = _getPayload();
payload['phone_number'] =
paymentController.walletphoneController.text;
var res = await CRUD().postWallet(
link: AppLink.payWithWallet,
payload: payload,
);
if (res != null && res['message'] != null) {
Get.back();
Get.to(() => GenericPaymentWebView(
iframeUrl: res['message'],
countPrice: widget.countPoint,
isWallet: true,
));
} else {
setStateDialog(() => isDialogLoading = false);
mySnackeBarError("Failed to get payment link".tr);
}
} catch (e) {
if (mounted) {
setStateDialog(() => isDialogLoading = false);
}
mySnackeBarError(
"Failed to initialize wallet payment".tr);
}
}
},
child: isDialogLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: Text('Confirm & Pay'.tr,
style: const TextStyle(color: Colors.white)),
),
),
const SizedBox(height: 10),
if (!isDialogLoading)
TextButton(
onPressed: () => Get.back(),
child: Text("Cancel".tr,
style: const TextStyle(color: Colors.grey)),
)
],
);
}),
);
}
}
// -----------------------------------------------------------------------------
// صفحة الويب (WebView) مع اللودينج الخاص بها
// -----------------------------------------------------------------------------
class GenericPaymentWebView extends StatefulWidget {
final String iframeUrl;
final String countPrice;
final bool isWallet;
const GenericPaymentWebView({
Key? key,
required this.iframeUrl,
required this.countPrice,
required this.isWallet,
}) : super(key: key);
@override
State<GenericPaymentWebView> createState() => _GenericPaymentWebViewState();
}
class _GenericPaymentWebViewState extends State<GenericPaymentWebView> {
late final WebViewController _controller;
final controller = Get.find<CaptainWalletController>();
bool isLoadingPage = true;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageStarted: (_) {
if (mounted) setState(() => isLoadingPage = true);
},
onPageFinished: (url) {
if (mounted) setState(() => isLoadingPage = false);
if (url.contains("success")) {
_fetchPaymentStatus();
} else if (url.contains("failed") || url.contains("error")) {
showCustomDialog(
title: "Error".tr,
message: 'Payment Failed'.tr,
isSuccess: false,
);
}
},
onWebResourceError: (error) {
if (mounted) setState(() => isLoadingPage = false);
},
))
..loadRequest(Uri.parse(widget.iframeUrl));
}
Future<void> _fetchPaymentStatus() async {
final String userId = widget.isWallet
? '+2${box.read(BoxName.phoneWallet)}'
: box.read(BoxName.phoneDriver);
Get.dialog(const Center(child: CircularProgressIndicator()),
barrierDismissible: false);
try {
final response = await CRUD().postWallet(
link: AppLink.paymetVerifyDriver,
payload: {
'user_id': userId,
'driverID': box.read(BoxName.driverID),
'paymentMethod': 'visa-in',
},
);
Get.back(); // إغلاق لودينج التحقق
if (response != 'failure' && response != 'token_expired') {
if (response['status'] == 'success') {
final payment = response['message'];
final amount = payment['amount'].toString();
final bonus = payment['bonus'].toString();
final paymentID = payment['paymentID'].toString();
await controller.refreshCaptainWallet();
showCustomDialog(
title: "payment_success".tr,
message:
"${"transaction_id".tr}: $paymentID\n${"amount_paid".tr}: $amount EGP\n${"bonus_added".tr}: $bonus ${"points".tr}",
isSuccess: true,
shouldCloseScreen: true,
);
} else {
showCustomDialog(
title: "transaction_failed".tr,
message: response['message'].toString(),
isSuccess: false,
);
}
} else {
showCustomDialog(
title: "connection_failed".tr,
message: "Could not verify payment status".tr,
isSuccess: false,
);
}
} catch (e) {
Get.back();
showCustomDialog(
title: "server_error".tr,
message: "server_error_message".tr,
isSuccess: false,
);
}
}
void showCustomDialog({
required String title,
required String message,
required bool isSuccess,
bool shouldCloseScreen = false,
}) {
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSuccess
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
),
child: Icon(
isSuccess ? Icons.check_circle : Icons.error_outline,
color: isSuccess ? Colors.green : Colors.red,
size: 40,
),
),
const SizedBox(height: 15),
Text(
title,
style: AppStyle.headTitle2.copyWith(
color: isSuccess ? Colors.green : Colors.red,
),
),
const SizedBox(height: 10),
Text(
message,
textAlign: TextAlign.center,
style: AppStyle.title,
),
const SizedBox(height: 25),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isSuccess ? Colors.green : Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
Get.back();
if (shouldCloseScreen) {
Get.back();
}
},
child: Text("OK".tr,
style: const TextStyle(color: Colors.white)),
),
)
],
),
),
),
barrierDismissible: false,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Secure Payment'.tr),
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (isLoadingPage)
Container(
color: Colors.white,
width: double.infinity,
height: double.infinity,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text("Loading Payment Page...".tr,
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 16)),
],
),
),
),
],
),
);
}
}
@@ -0,0 +1,145 @@
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:sefer_driver/views/widgets/my_textField.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/home/payment/captain_wallet_controller.dart';
class TransferBudgetPage extends StatelessWidget {
const TransferBudgetPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(CaptainWalletController());
return MyScafolld(
title: "Transfer budget".tr,
body: [
GetBuilder<CaptainWalletController>(
builder: (captainWalletController) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: AppStyle.boxDecoration1,
height: Get.height * .7,
width: double.infinity,
child: Form(
key: captainWalletController.formKeyTransfer,
child: Column(
children: [
const SizedBox(
height: 20,
),
MyTextForm(
controller: captainWalletController
.newDriverPhoneController,
label: 'phone number of driver'.tr,
hint: 'phone number of driver',
type: TextInputType.phone),
MyTextForm(
controller: captainWalletController
.amountFromBudgetController,
label: 'insert amount'.tr,
hint:
'${'You have in account'.tr} ${captainWalletController.totalAmountVisa}',
type: TextInputType.number),
captainWalletController.isNewTransfer
? const MyCircularProgressIndicator()
: captainWalletController
.amountToNewDriverMap.isEmpty
? MyElevatedButton(
title: 'Next'.tr,
onPressed: () async {
await captainWalletController
.detectNewDriverFromMyBudget();
})
: const SizedBox(),
captainWalletController.amountToNewDriverMap.isNotEmpty
? Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Container(
width: double.maxFinite,
decoration: AppStyle.boxDecoration1,
child: Text(
'Name :'.tr +
captainWalletController
.amountToNewDriverMap[0]['name']
.toString(),
textAlign: TextAlign.center,
style: AppStyle.title,
),
),
const SizedBox(
height: 5,
),
Container(
width: double.maxFinite,
decoration: AppStyle.boxDecoration1,
child: Text(
"${"NationalID".tr} ${captainWalletController.amountToNewDriverMap[0]['national_number']}",
style: AppStyle.title,
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 5,
),
Container(
width: double.maxFinite,
decoration: AppStyle.boxDecoration1,
child: Text(
"${"amount".tr} ${captainWalletController.amountFromBudgetController.text} ${'LE'.tr}",
style: AppStyle.title,
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 15,
),
captainWalletController
.amountToNewDriverMap.isNotEmpty
? MyElevatedButton(
title: 'Transfer'.tr,
onPressed: () async {
if (double.parse(
captainWalletController
.amountFromBudgetController
.text) <
double.parse(
captainWalletController
.totalAmountVisa) -
5) {
await captainWalletController
.addTransferDriversWallet(
'TransferFrom',
'TransferTo',
);
} else {
MyDialog().getDialog(
"You dont have money in your Wallet"
.tr,
"You dont have money in your Wallet or you should less transfer 5 LE to activate"
.tr, () {
Get.back();
});
}
})
: const SizedBox()
],
),
)
: const SizedBox()
],
)),
),
);
}),
],
isleading: true);
}
}
+728
View File
@@ -0,0 +1,728 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:local_auth/local_auth.dart';
import 'dart:ui';
// --- IMPORTS ---
import 'package:sefer_driver/constant/links.dart';
import 'package:sefer_driver/controller/functions/crud.dart';
import 'package:sefer_driver/controller/home/payment/paymob_payout.dart';
import 'package:sefer_driver/views/home/my_wallet/bank_account_egypt.dart';
import 'package:sefer_driver/views/home/my_wallet/payment_history_driver_page.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_textField.dart';
import '../../../controller/payment/driver_payment_controller.dart';
import 'card_wallet_widget.dart';
import 'points_captain.dart';
import 'transfer_budget_page.dart';
import 'weekly_payment_page.dart';
// --- COLOR PALETTE (Modern & Professional) ---
const kBgColor = Color(0xFFF8F9FA); // خلفية فاتحة مريحة للعين
const kPrimaryColor = Color(0xFF0056D2); // أزرق احترافي (يشبه PayPal/Visa)
const kCardColor = Colors.white;
const kTextPrimary = Color(0xFF2D3436);
const kTextSecondary = Color(0xFF636E72);
const kGreenSuccess = Color(0xFF00B894);
const kRedDanger = Color(0xFFD63031);
const kGold = Color(0xFFFDCB6E);
class WalletCaptainRefactored extends StatelessWidget {
WalletCaptainRefactored({super.key});
final CaptainWalletController captainWalletController =
Get.put(CaptainWalletController());
// تحديد لون حالة النقاط (أخضر = أمان، أحمر = خطر وقف حال)
Color _getPointsStatusColor(String pointsStr) {
final points = double.tryParse(pointsStr) ?? 0.0;
if (points < -300) return kRedDanger;
if (points < 0) return Colors.orange;
return kGreenSuccess;
}
@override
Widget build(BuildContext context) {
captainWalletController.refreshCaptainWallet();
return Scaffold(
backgroundColor: kBgColor,
appBar: AppBar(
backgroundColor: kCardColor,
elevation: 0,
centerTitle: true,
title: const Text(
'محفظة الكابتن',
style: TextStyle(color: kTextPrimary, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: kTextPrimary),
onPressed: () => Get.back(),
),
actions: [
IconButton(
icon: const Icon(Icons.refresh_rounded, color: kPrimaryColor),
onPressed: () => captainWalletController.refreshCaptainWallet(),
)
],
),
body: GetBuilder<CaptainWalletController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(
child: CircularProgressIndicator(color: kPrimaryColor));
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. كارت النقاط (حالة الحساب)
_buildPointsStatusCard(context, controller),
const SizedBox(height: 20),
const CardTripzWalletDriver(), // Kept as is.
const SizedBox(height: 20),
// 2. ملخص الرصيد (كاش وفيزا)
const Text("ملخص الرصيد",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: _buildBalanceCard(
title: "المديونية (كاش معك)",
amount: controller.totalAmount,
icon: Icons.money_off_csred_rounded,
color: kRedDanger,
subtitle: "مبالغ تم تحصيلها كاش",
)),
const SizedBox(width: 12),
Expanded(
child: _buildBalanceCard(
title: "رصيدك بالمحفظة",
amount: controller.totalAmountVisa,
icon: Icons.account_balance_wallet,
color: kGreenSuccess,
subtitle: "رصيد قابل للسحب/التحويل",
)),
],
),
const SizedBox(height: 20),
// 3. الإجراءات السريعة (Quick Actions)
const Text("إجراءات سريعة",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
_buildQuickActionsGrid(context, controller),
const SizedBox(height: 20),
// 4. إعدادات السحب (Payout Settings) - مهم جداً لمصر
_buildPayoutSection(context, controller),
const SizedBox(height: 20),
// 5. البونص والعروض
if (controller.walletDate['message'] != null)
_buildPromoSection(controller),
const SizedBox(height: 20),
// 6. شحن النقاط
const Text("باقات شحن النقاط",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
_buildPointsPackages(),
const SizedBox(height: 30),
],
),
);
},
),
);
}
// --- WIDGETS ---
Widget _buildPointsStatusCard(
BuildContext context, CaptainWalletController controller) {
Color statusColor =
_getPointsStatusColor(controller.totalPoints.toString());
bool isBlocked = double.parse(controller.totalPoints.toString()) < -300;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isBlocked
? [kRedDanger, Colors.red.shade300]
: [kPrimaryColor, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: (isBlocked ? kRedDanger : kPrimaryColor).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5))
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("رصيد التشغيل (النقاط)",
style: TextStyle(
color: Colors.white.withOpacity(0.9), fontSize: 16)),
InkWell(
onTap: () => _showInfoDialog(context),
child: const Icon(Icons.info_outline, color: Colors.white),
)
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
controller.totalPoints.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold),
),
const SizedBox(width: 5),
const Padding(
padding: EdgeInsets.only(bottom: 8.0),
child: Icon(Icons.diamond, color: kGold, size: 24),
),
],
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
isBlocked
? "حسابك موقوف مؤقتاً - اشحن فوراً"
: "حالة الحساب: نشط ✅",
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
);
}
Widget _buildBalanceCard(
{required String title,
required String amount,
required IconData icon,
required Color color,
required String subtitle}) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 5,
offset: const Offset(0, 2))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 10),
Text(title,
style: TextStyle(
color: kTextSecondary,
fontSize: 12,
fontWeight: FontWeight.w600)),
const SizedBox(height: 5),
Text("$amount ج.م",
style: TextStyle(
color: kTextPrimary,
fontSize: 18,
fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Text(subtitle,
style: TextStyle(color: Colors.grey.shade400, fontSize: 10)),
],
),
);
}
Widget _buildQuickActionsGrid(
BuildContext context, CaptainWalletController controller) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 1.1,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
children: [
_buildActionButton(
icon: Icons.swap_horiz_rounded,
label: "تحويل نقاط",
color: Colors.orange,
onTap: () => _showBuyFromBudgetDialog(controller),
),
_buildActionButton(
icon: Icons.send_to_mobile_rounded,
label: "تحويل لكابتن",
color: Colors.purple,
onTap: () => _handleTransferBudget(controller),
),
_buildActionButton(
icon: Icons.history_rounded,
label: "سجل العمليات",
color: Colors.blueGrey,
onTap: () async {
// await Get.put(DriverWalletHistoryController()).getArchivePayment();
Get.to(() => const PaymentHistoryDriverPage());
},
),
],
);
}
Widget _buildActionButton(
{required IconData icon,
required String label,
required Color color,
required VoidCallback onTap}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(15),
child: Container(
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
backgroundColor: color.withOpacity(0.1),
radius: 22,
child: Icon(icon, color: color),
),
const SizedBox(height: 8),
Text(label,
style:
const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
textAlign: TextAlign.center),
],
),
),
);
}
Widget _buildPayoutSection(
BuildContext context, CaptainWalletController controller) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: kPrimaryColor.withOpacity(0.2)),
boxShadow: [
BoxShadow(color: kPrimaryColor.withOpacity(0.05), blurRadius: 10)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Icon(Icons.credit_card_rounded, color: kPrimaryColor),
SizedBox(width: 10),
Text("إعدادات سحب الرصيد (Payout)",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 10),
const Text(
"عشان تقدر تسحب فلوسك، لازم تضيف حساب بنكي أو محفظة إلكترونية.",
style: TextStyle(fontSize: 12, color: kTextSecondary)),
const SizedBox(height: 15),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
// استدعاء دالة إضافة الحساب البنكي الموجودة أصلاً
addBankCodeEgypt(controller);
},
icon: const Icon(Icons.add_card, size: 18),
label: const Text("إضافة حساب بنكي / محفظة",
style: TextStyle(fontWeight: FontWeight.bold)),
),
),
const SizedBox(height: 10),
InkWell(
onTap: () async {
await Get.put(DriverWalletHistoryController())
.getWeekllyArchivePayment();
Get.to(() => const WeeklyPaymentPage());
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text("عرض الأرباح الأسبوعية",
style: TextStyle(
color: kPrimaryColor, fontWeight: FontWeight.bold)),
Icon(Icons.arrow_forward_ios, size: 12, color: kPrimaryColor),
],
),
)
],
),
);
}
Widget _buildPromoSection(CaptainWalletController controller) {
// التأكد من وجود البيانات لتجنب الأخطاء
int morningCount = 0;
int afternoonCount = 0;
try {
// morningCount = controller.walletDate['message'][0]['morning_count'] ?? 0;
morningCount = controller.morningCount;
afternoonCount = controller.afternoonCount;
} catch (e) {
// ignore
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("بونص اليوم 🎁",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: kTextPrimary)),
const SizedBox(height: 10),
_buildPromoCard(
title: "بونص الصبحية",
desc: "اعمل 5 رحلات من 7ص لـ 10ص",
current: morningCount,
max: 5,
color: Colors.orange,
onTap: () => controller.addDriverWalletFromPromo('Morning Promo', 50),
),
const SizedBox(height: 10),
_buildPromoCard(
title: "بونص آخر النهار",
desc: "اعمل 5 رحلات من 3م لـ 6م",
current: afternoonCount,
max: 5,
color: Colors.indigo,
onTap: () =>
controller.addDriverWalletFromPromo('Afternoon Promo', 50),
),
],
);
}
Widget _buildPromoCard(
{required String title,
required String desc,
required int current,
required int max,
required Color color,
required VoidCallback onTap}) {
double progress = (current / max).clamp(0.0, 1.0);
return InkWell(
onTap: () {
if (current >= max) {
onTap();
} else {
Get.snackbar("شد حيلك", "لسه فاضلك رحلات عشان تاخد البونص");
}
},
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
children: [
CircularProgressIndicator(
value: progress,
backgroundColor: Colors.grey.shade200,
color: color,
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(fontWeight: FontWeight.bold)),
Text(desc,
style:
const TextStyle(fontSize: 11, color: kTextSecondary)),
],
),
),
if (current >= max)
const Icon(Icons.check_circle, color: kGreenSuccess)
else
Text("$current/$max",
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
],
),
),
);
}
Widget _buildPointsPackages() {
return SizedBox(
height: 140,
child: ListView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
children: [
_buildPackageCard(price: 100, points: "100", color: Colors.blueGrey),
_buildPackageCard(
price: 200,
points: "210",
color: const Color(0xFFCD7F32)), // Bronze
_buildPackageCard(
price: 400, points: "435", color: Colors.grey), // Silver
_buildPackageCard(
price: 1000,
points: "1100",
color: const Color(0xFFFFD700)), // Gold
],
),
);
}
Widget _buildPackageCard(
{required double price, required String points, required Color color}) {
return Padding(
padding: const EdgeInsets.only(right: 10),
child: PointsCaptain(
kolor: color,
pricePoint: price,
countPoint: points,
),
);
}
// --- LOGIC DIALOGS ---
void _showInfoDialog(BuildContext context) {
showCupertinoDialog(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('نظام النقاط'),
content: const Text(
'عشان تستقبل رحلات لازم رصيدك يكون أكبر من -300 جنيه.\nلو الرصيد قل عن كده، التطبيق هيوقف استقبال الطلبات لحد ما تشحن.'),
actions: [
CupertinoDialogAction(
child: const Text("تمام"), onPressed: () => Navigator.pop(ctx))
],
),
);
}
void _showBuyFromBudgetDialog(CaptainWalletController controller) {
Get.defaultDialog(
title: 'تحويل من المحفظة للنقاط',
content: Form(
key: controller.formKey,
child: Column(
children: [
Text("رصيدك الحالي بالمحفظة: ${controller.totalAmountVisa} ج.م",
style: const TextStyle(
color: kGreenSuccess, fontWeight: FontWeight.bold)),
const SizedBox(height: 15),
MyTextForm(
controller: controller.amountFromBudgetController,
label: 'المبلغ المراد تحويله',
hint: 'أدخل المبلغ',
type: TextInputType.number,
),
],
),
),
confirm: MyElevatedButton(
title: 'تأكيد التحويل',
onPressed: () async {
await _authenticateAndExecute(() async {
if (double.parse(controller.amountFromBudgetController.text) <=
double.parse(controller.totalAmountVisa.toString())) {
await controller.payFromBudget();
} else {
Get.back();
mySnackeBarError('رصيد المحفظة غير كافي للعملية');
}
});
},
),
cancel:
TextButton(onPressed: () => Get.back(), child: const Text("إلغاء")),
);
}
void _handleTransferBudget(CaptainWalletController controller) async {
await _authenticateAndExecute(() async {
if (double.parse(controller.totalAmountVisa) > 15) {
Get.to(() => const TransferBudgetPage());
} else {
mySnackeBarError("رصيد محفظتك لا يكفي للتحويل (الحد الأدنى 15 ج.م)");
}
});
}
Future<void> _authenticateAndExecute(Function onSuccess) async {
bool isAvailable = await LocalAuthentication().isDeviceSupported();
if (isAvailable) {
bool didAuthenticate = await LocalAuthentication().authenticate(
localizedReason: 'يرجى استخدام البصمة لتأكيد العملية المالية',
options: const AuthenticationOptions(
biometricOnly: true, sensitiveTransaction: true),
);
if (didAuthenticate) {
onSuccess();
} else {
MyDialog().getDialog(
'فشل التحقق', 'لم يتم التعرف على البصمة', () => Get.back());
}
} else {
// في حالة عدم وجود بصمة، يمكن تنفيذ العملية مباشرة أو طلب كلمة مرور (حسب رغبتك)
// هنا سننفذها للتسهيل
// onSuccess();
}
}
}
// --- الدوال القديمة (كما هي للإبقاء على اللوجيك) ---
Future<dynamic> addBankCodeEgypt(
CaptainWalletController captainWalletController) {
return Get.defaultDialog(
title: "بيانات السحب (Payout Info)",
content: Form(
key: captainWalletController.formKeyAccount,
child: Column(
children: [
const Text(
"أدخل بيانات حسابك البنكي أو المحفظة لاستقبال أرباحك الأسبوعية",
textAlign: TextAlign.center),
const SizedBox(height: 15),
MyTextForm(
controller: captainWalletController.cardBank,
label: "رقم الحساب / المحفظة / الآيبان",
hint: 'مثال: 010xxxxxxx أو رقم الحساب',
type: TextInputType.number,
),
const SizedBox(height: 10),
BankDropdown() // تأكد من أن هذا الودجت يعرض البنوك والمحافظ المصرية
],
),
),
confirm: MyElevatedButton(
title: 'حفظ البيانات',
onPressed: () async {
if (captainWalletController.formKeyAccount.currentState!.validate()) {
Get.back();
// هنا يتم إرسال البيانات للسيرفر
var res =
await CRUD().post(link: AppLink.updateAccountBank, payload: {
"bankCode": Get.find<BankController>().selectedBank.toString(),
"accountBank": captainWalletController.cardBank.text.toString(),
"id": box.read(BoxName.driverID).toString()
});
if (res != 'failure') {
mySnackbarSuccess('تم حفظ بيانات الدفع بنجاح');
}
}
},
),
);
}
// الودجت الخاص باختيار المحفظة (فودافون كاش، اتصالات، إلخ)
class MyDropDown1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
Get.put(PaymobPayout());
return GetBuilder<PaymobPayout>(builder: (controller) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: controller.dropdownValue,
icon: const Icon(Icons.keyboard_arrow_down_rounded),
style: const TextStyle(
color: kTextPrimary, fontWeight: FontWeight.bold),
onChanged: (String? newValue) {
controller.dropdownValue = newValue!;
controller.update();
},
items: <String>[
'etisalat',
'aman',
'orange',
'vodafone',
'Bank Account'
].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value), // يمكن تعريب الأسماء هنا إذا أردت
);
}).toList(),
),
),
);
});
}
}
@@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../controller/payment/driver_payment_controller.dart';
class WeeklyPaymentPage extends StatelessWidget {
const WeeklyPaymentPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(DriverWalletHistoryController());
return Scaffold(
appBar: AppBar(
title: Text('Weekly Summary'.tr),
backgroundColor: Colors.white,
elevation: 1,
),
backgroundColor: Colors.grey[100],
body: GetBuilder<DriverWalletHistoryController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
return Column(
children: [
// 1. Prominent Summary Card at the top
_buildSummaryCard(controller),
// 2. A title for the transactions list
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
child: Row(
children: [
Icon(Icons.list_alt, color: Colors.grey[600]),
const SizedBox(width: 8),
Text(
'Transactions this week'.tr,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
],
),
),
// 3. The animated list of transactions
Expanded(
child: controller.weeklyList.isEmpty
? _buildEmptyState(context)
: AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
itemCount: controller.weeklyList.length,
itemBuilder: (BuildContext context, int index) {
var transaction = controller.weeklyList[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: _TransactionListItem(
transaction: transaction),
),
),
);
},
),
),
),
],
);
},
),
);
}
// A widget for the top summary card.
Widget _buildSummaryCard(DriverWalletHistoryController controller) {
final String totalAmount = controller.weeklyList.isEmpty
? '0.00'
: controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
return Card(
margin: const EdgeInsets.all(16.0),
elevation: 4,
shadowColor: AppColor.primaryColor.withOpacity(0.2),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: Container(
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColor.primaryColor, AppColor.greenColor],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
const Icon(Icons.account_balance_wallet,
color: Colors.white, size: 40),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Weekly Earnings'.tr,
style: AppStyle.title
.copyWith(color: Colors.white70, fontSize: 16),
),
const SizedBox(height: 4),
Text(
'$totalAmount ${'SYP'.tr}',
style: AppStyle.number
.copyWith(color: Colors.white, fontSize: 32),
),
],
),
),
],
),
),
);
}
// A dedicated widget for the list item.
Widget _TransactionListItem({required Map<String, dynamic> transaction}) {
final String paymentMethod = transaction['paymentMethod'] ?? 'Unknown';
final String amount = transaction['amount']?.toString() ?? '0';
final DateTime? date = DateTime.tryParse(transaction['dateUpdated'] ?? '');
return Card(
elevation: 2,
shadowColor: Colors.black.withOpacity(0.05),
margin: const EdgeInsets.only(bottom: 12.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: ListTile(
leading: CircleAvatar(
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
child: Icon(_getPaymentIcon(paymentMethod),
color: AppColor.primaryColor, size: 22),
),
title: Text(
'$amount ${'SYP'.tr}',
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
),
subtitle: Text(
date != null
? DateFormat('EEEE, hh:mm a', Get.locale?.toString())
.format(date) // e.g., Tuesday, 10:11 AM
: 'Invalid Date',
style: AppStyle.title.copyWith(fontSize: 12, color: Colors.grey[600]),
),
trailing: Chip(
label: Text(
_getTranslatedPaymentMethod(paymentMethod),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
backgroundColor: Colors.grey.shade200,
padding: const EdgeInsets.symmetric(horizontal: 6),
side: BorderSide.none,
),
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long_outlined, size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No transactions this week'.tr,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: Colors.grey[600]),
),
],
),
);
}
// Helper to get a specific icon for each payment method.
IconData _getPaymentIcon(String paymentMethod) {
switch (paymentMethod.toLowerCase()) {
case 'visa':
return Icons.credit_card;
case 'frombudget':
return Icons.account_balance_wallet_outlined;
case 'remainder':
return Icons.receipt_long;
case 'cash':
return Icons.money;
default:
return Icons.payment;
}
}
// Helper to get translated or formatted payment method names.
String _getTranslatedPaymentMethod(String paymentMethod) {
switch (paymentMethod) {
case 'Remainder':
return 'Remainder'.tr;
case 'fromBudget':
return 'From Budget'.tr;
default:
return paymentMethod;
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/auth/onboarding_controller.dart';
import 'package:sefer_driver/onbording_page.dart'; // تأكد من مسار الـ Widgets الداخلية
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
class OnBoardingPage extends StatelessWidget {
const OnBoardingPage({super.key});
@override
Widget build(BuildContext context) {
// حقن الكونترولر مرة واحدة
final OnBoardingControllerImp controller =
Get.put(OnBoardingControllerImp());
return Scaffold(
backgroundColor: Colors.white, // خلفية بيضاء ثابتة لمنع الرمش
body: SafeArea(
child: Column(
children: [
// مساحة علوية مرنة
const Spacer(flex: 1),
// منطقة العرض الرئيسية (Slider)
SizedBox(
height: Get.height *
0.6, // تقليل الارتفاع قليلاً لتناسب الشاشات المختلفة
child: const CustomSliderOnBoarding(),
),
// منطقة النقاط (Dots)
const SizedBox(height: 20),
const CustomDotControllerOnBoarding(),
// مساحة سفلية
const Spacer(flex: 2),
// زر المتابعة
Padding(
padding: const EdgeInsets.only(bottom: 30.0),
child: GetBuilder<OnBoardingControllerImp>(
builder: (controller) => MyElevatedButton(
title: controller.isLastPage ? "Start".tr : "Next".tr,
onPressed: () => controller.next(),
// width: Get.width * 0.8, // عرض ثابت للزر
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/home/captin/behavior_controller.dart';
class BehaviorPage extends StatelessWidget {
const BehaviorPage({
super.key,
});
@override
Widget build(BuildContext context) {
final controller = Get.put(DriverBehaviorController());
controller.fetchDriverBehavior();
return Scaffold(
appBar: AppBar(
title: Text('Driver Behavior'.tr),
centerTitle: true,
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text("Overall Behavior Score".tr,
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Text(
"${controller.overallScore.value.toStringAsFixed(1)} / 100",
style: const TextStyle(
fontSize: 28, color: Colors.blue)),
],
),
),
),
const SizedBox(height: 20),
Text("Last 10 Trips".tr,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.lastTrips.length,
itemBuilder: (context, index) {
var trip = controller.lastTrips[index];
return Card(
elevation: 3,
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue,
child: Text("${index + 1}",
style: const TextStyle(color: Colors.white)),
),
title: Text("Trip ID: ${trip['trip_id']}"),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${'Behavior Score'.tr}: ${trip['behavior_score']}"),
Text("${'Max Speed'.tr}: ${trip['max_speed']} km/h"),
Text("${'Hard Brake'.tr}s: ${trip['hard_brakes']}"),
Text(
"${'Distance'.tr}: ${trip['total_distance']} km"),
],
),
),
);
},
),
],
),
);
}),
);
}
}
+130
View File
@@ -0,0 +1,130 @@
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
import 'package:sefer_driver/main.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 'package:flutter/material.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import '../../auth/captin/driver_car_controller.dart';
import '../../widgets/elevated_btn.dart';
import 'cars_inserting_page.dart';
class CaptainsCars extends StatelessWidget {
const CaptainsCars({super.key});
@override
Widget build(BuildContext context) {
Get.put(DriverCarController());
return MyScafolld(
title: "Add new car".tr,
body: [
Column(
children: [
MyElevatedButton(
title: "Add new car".tr,
onPressed: () async {
Get.to(() => CarsInsertingPage());
},
),
Expanded(
child: GetBuilder<DriverCarController>(
builder: (controller) {
return controller.isLoading
? const MyCircularProgressIndicator()
: ListView.builder(
itemCount: controller.cars.length,
itemBuilder: (context, index) {
final car = controller.cars[index];
return Padding(
padding: const EdgeInsets.all(4.0),
child: Card(
color: car['isDefault'].toString() == '0'
? AppColor.accentColor
: AppColor.blueColor,
elevation: 2,
child: ListTile(
leading: Icon(
Fontisto.car,
size: 50,
color: Color(int.parse(car['color_hex']
.replaceFirst('#', '0xff'))),
),
title: Text(
car['make'],
style: AppStyle.title,
), // Assuming `make` is a field in each car item
subtitle: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
car['model'],
style: AppStyle.title,
),
Container(
decoration: BoxDecoration(
border: Border.all(
color: AppColor.blueColor)),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4),
child: Text(
(car['car_plate']),
style: AppStyle.title,
),
),
),
Text(
car['year'],
style: AppStyle.title,
),
],
), // Assuming `model` is a field in each car item
// trailing: IconButton(
// icon: const Icon(
// Icons.delete,
// color: AppColor.redColor,
// ),
// onPressed: () {
// // Add logic here to remove a car
// MyDialog()
// .getDialog('Are you sure to delete this car', '', () {
// controller
// .removeCar(car['id'].toString());
// });
// },
// ),
onTap: () {
MyDialog().getDialog(
'Are you sure to make this car as default'
.tr,
'', () {
Get.back();
//make it default
controller.updateCarRegistration(
car['id'].toString(),
box.read(BoxName.driverID).toString(),
);
});
// Add logic to view or edit the car details
},
),
),
);
},
);
},
),
),
],
)
],
isleading: true);
}
}
+308
View File
@@ -0,0 +1,308 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../auth/captin/ai_eg_controller.dart';
import '../../auth/captin/driver_car_controller.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
class CarsInsertingPage extends StatelessWidget {
CarsInsertingPage({super.key});
// حقن الكنترولرز
final driverCarController = Get.put(DriverCarController());
final aiController = Get.put(AI());
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'Insert New Car'.tr,
isleading: true,
body: [
Container(
color: AppColor.accentColor.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: [
// 1. الوجه الأمامي (الذكاء الاصطناعي)
egyptCarLicenceFront(),
// 2. الوجه الخلفي (الذكاء الاصطناعي)
egyptCarLicenceBack(),
const SizedBox(height: 10),
// تعليمات
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Please make sure to read the license carefully.'.tr,
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Text(
'If your car license has the new design, upload the front side with two images.'
.tr,
style: const TextStyle(fontSize: 12)),
],
),
),
const SizedBox(height: 20),
// 3. زر الحفظ النهائي
GetBuilder<AI>(builder: (controller) {
// التحقق من أن الصور تم رفعها
bool isFrontReady =
controller.responseIdCardDriverEgyptFront.isNotEmpty;
bool isBackReady =
controller.responseIdCardDriverEgyptBack.isNotEmpty;
return MyElevatedButton(
title: 'Save New Car'.tr,
// تعطيل الزر إذا لم تكتمل البيانات
onPressed: _handleSaveCar,
kolor: (isFrontReady && isBackReady)
? AppColor.primaryColor
: Colors.grey,
);
})
],
),
),
)
],
);
}
// منطق الحفظ (يربط الـ AI بالـ DriverCarController)
void _handleSaveCar() {
final aiFront = Get.find<AI>().responseIdCardDriverEgyptFront;
final aiBack = Get.find<AI>().responseIdCardDriverEgyptBack;
// استدعاء دالة الإضافة في كنترولر السيارات
Get.find<DriverCarController>().addNewCarToAccount(
vin: aiBack['chassis'].toString(),
plate: aiFront['car_plate'].toString(), // اللوحة من الأمام
make: aiBack['make'].toString(),
model: aiBack['model'].toString(),
year: aiBack['year'].toString(),
expiryDate: aiFront['LicenseExpirationDate'].toString(),
color: aiBack['color'].toString(),
colorHex: aiBack['color_hex'].toString(),
owner: aiFront['owner'].toString(),
address: aiFront['address'].toString(),
inspectionDate: aiBack['inspection_date'].toString(), // تاريخ الفحص
displacement: aiBack['displacement'].toString(), // السعة اللترية
fuel: aiBack['fuel'].toString(),
);
}
}
// --- Widget الوجه الأمامي ---
GetBuilder<AI> egyptCarLicenceFront() {
return GetBuilder<AI>(
builder: (ai) {
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
final licenseExpiryDate = DateTime.tryParse(
ai.responseIdCardDriverEgyptFront['LicenseExpirationDate'] ??
'') ??
DateTime.now();
final isLicenseExpired = licenseExpiryDate.isBefore(DateTime.now());
return Card(
elevation: 4.0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Vehicle Details Front'.tr,
style: AppStyle.headTitle2),
IconButton(
// إعادة الرفع
onPressed: () => ai.allMethodForAI('car_front'),
icon: const Icon(Icons.refresh,
color: AppColor.primaryColor),
),
],
),
const Divider(color: AppColor.accentColor),
_infoRow('Plate Number'.tr,
ai.responseIdCardDriverEgyptFront['car_plate']),
_infoRow('Owner Name'.tr,
ai.responseIdCardDriverEgyptFront['owner']),
_infoRow(
'Address'.tr, ai.responseIdCardDriverEgyptFront['address']),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('License Expiry Date'.tr),
Text(
licenseExpiryDate.toString().split(' ')[0],
style: TextStyle(
fontWeight: FontWeight.bold,
color: isLicenseExpired ? Colors.red : Colors.green,
),
),
],
),
),
],
),
),
);
}
// حالة الانتظار (زر الرفع)
return Card(
child: InkWell(
onTap: () => ai.allMethodForAI('car_front'), // ✅ هنا التفعيل
child: Column(
children: [
Image.asset(
'assets/images/3.png',
height: Get.height * .20,
fit: BoxFit.contain,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Capture Car License (Front)'.tr,
style: AppStyle.title),
),
],
),
),
);
},
);
}
// --- Widget الوجه الخلفي ---
GetBuilder<AI> egyptCarLicenceBack() {
return GetBuilder<AI>(
builder: (ai) {
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
// منطق تواريخ الفحص والضريبة
final inspectionDateStr =
ai.responseIdCardDriverEgyptBack['inspection_date'] ?? '';
return Card(
elevation: 4.0,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Vehicle Details Back'.tr, style: AppStyle.headTitle2),
IconButton(
// إعادة الرفع
onPressed: () => ai.allMethodForAI('car_back'),
icon: const Icon(Icons.refresh,
color: AppColor.primaryColor),
),
],
),
const Divider(color: AppColor.accentColor),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: _infoRow('Make'.tr,
ai.responseIdCardDriverEgyptBack['make'])),
Expanded(
child: _infoRow('Model'.tr,
ai.responseIdCardDriverEgyptBack['model'])),
],
),
_infoRow('Year'.tr, ai.responseIdCardDriverEgyptBack['year']),
_infoRow(
'Chassis'.tr, ai.responseIdCardDriverEgyptBack['chassis']),
Row(
children: [
Expanded(
child: _infoRow('Color'.tr,
ai.responseIdCardDriverEgyptBack['color'])),
// عرض دائرة اللون
if (ai.responseIdCardDriverEgyptBack['color_hex'] != null)
Container(
width: 20,
height: 20,
margin: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: Color(int.parse(ai
.responseIdCardDriverEgyptBack['color_hex']
.toString()
.replaceFirst('#', '0xff'))),
shape: BoxShape.circle,
border: Border.all(color: Colors.grey)),
)
],
),
_infoRow('Displacement'.tr,
"${ai.responseIdCardDriverEgyptBack['displacement']} cc"),
_infoRow('Fuel'.tr, ai.responseIdCardDriverEgyptBack['fuel']),
_infoRow('Inspection Date'.tr, inspectionDateStr),
],
),
),
);
}
// حالة الانتظار
return Card(
child: InkWell(
onTap: () => ai.allMethodForAI('car_back'), // ✅ هنا التفعيل
child: Column(
children: [
Image.asset(
'assets/images/4.png',
height: Get.height * .20,
fit: BoxFit.contain,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Capture Car License (Back)'.tr,
style: AppStyle.title),
),
],
),
),
);
},
);
}
// Helper Widget
Widget _infoRow(String label, String? value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("$label:", style: const TextStyle(color: Colors.grey)),
Expanded(
child: Text(value ?? "---",
textAlign: TextAlign.end,
style: const TextStyle(fontWeight: FontWeight.bold)),
),
],
),
);
}
+60
View File
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/profile/feed_back_controller.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../widgets/elevated_btn.dart';
class FeedBackPage extends StatelessWidget {
FeedBackPage({super.key});
FeedBackController feedBackController = Get.put(FeedBackController());
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'Feed Back'.tr,
body: [
Padding(
padding: const EdgeInsets.all(26),
child: Form(
key: feedBackController.formKey,
child: Column(
children: [
TextFormField(
controller: feedBackController.feedbackController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'Enter your feedback here'.tr,
labelText: 'Feedback',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your feedback.';
}
return null;
},
),
const SizedBox(height: 20),
feedBackController.isLoading
? const MyCircularProgressIndicator()
: MyElevatedButton(
onPressed: () {
if (feedBackController.formKey.currentState!
.validate()) {
feedBackController.addFeedBack();
// Clear the feedback form
feedBackController.formKey.currentState!.reset();
}
},
title: 'Submit '.tr,
),
],
),
),
),
],
isleading: true,
);
}
}
@@ -0,0 +1,303 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/colors.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/controller/profile/profile_controller.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import 'package:sefer_driver/views/widgets/my_textField.dart';
import 'package:sefer_driver/views/widgets/mycircular.dart';
import '../../../controller/functions/log_out.dart';
class PassengerProfilePage extends StatelessWidget {
PassengerProfilePage({super.key});
LogOutController logOutController = Get.put(LogOutController());
@override
Widget build(BuildContext context) {
Get.put(ProfileController());
return MyScafolld(
isleading: true,
title: 'My Profile'.tr,
body: [
GetBuilder<ProfileController>(
builder: (controller) => controller.isloading
? const MyCircularProgressIndicator()
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: SizedBox(
height: Get.height,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Edit Profile'.tr,
style: AppStyle.headTitle2,
),
ListTile(
title: Text(
'Name'.tr,
style: AppStyle.title,
),
leading: const Icon(
Icons.person_pin_rounded,
size: 35,
),
trailing: const Icon(Icons.arrow_forward_ios),
subtitle: Text(
'${controller.prfoileData['first_name']} ${controller.prfoileData['last_name']}'),
onTap: () {
controller.updatField(
'first_name', TextInputType.name);
},
),
ListTile(
title: Text(
'Gender'.tr,
style: AppStyle.title,
),
leading: Image.asset(
'assets/images/gender.png',
width: 35,
),
trailing: const Icon(Icons.arrow_forward_ios),
subtitle: Text(
controller.prfoileData['gender'].toString()),
onTap: () {
Get.defaultDialog(
title: 'Update Gender'.tr,
content: Column(
children: [
GenderPicker(),
MyElevatedButton(
title: 'Update'.tr,
onPressed: () {
controller.updateColumn({
'id': controller.prfoileData['id']
.toString(),
'gender': controller.gender,
});
Get.back();
},
)
],
));
// controller.updatField('gender');
},
),
ListTile(
title: Text(
'Education'.tr,
style: AppStyle.title,
),
leading: Image.asset(
'assets/images/education.png',
width: 35,
),
trailing: const Icon(Icons.arrow_forward_ios),
subtitle: Text(controller.prfoileData['education']
.toString()),
onTap: () {
Get.defaultDialog(
barrierDismissible: true,
title: 'Update Education'.tr,
content: SizedBox(
height: 200,
child: Column(
children: [
EducationDegreePicker(),
],
),
),
confirm: MyElevatedButton(
title: 'Update Education'.tr,
onPressed: () {
controller.updateColumn({
'id': controller.prfoileData['id']
.toString(),
'education':
controller.selectedDegree,
});
Get.back();
},
));
},
),
ListTile(
title: Text(
'Employment Type'.tr,
style: AppStyle.title,
),
leading: Image.asset(
'assets/images/employmentType.png',
width: 35,
),
trailing: const Icon(Icons.arrow_forward_ios),
subtitle: Text(controller
.prfoileData['employmentType']
.toString()),
onTap: () {
controller.updatField(
'employmentType', TextInputType.name);
},
),
ListTile(
title: Text(
'Marital Status'.tr,
style: AppStyle.title,
),
leading: Image.asset(
'assets/images/maritalStatus.png',
width: 35,
),
trailing: const Icon(Icons.arrow_forward_ios),
subtitle: Text(controller
.prfoileData['maritalStatus']
.toString()),
onTap: () {
controller.updatField(
'maritalStatus', TextInputType.name);
},
),
ListTile(
title: Text(
'SOS Phone'.tr,
style: AppStyle.title,
),
leading: const Icon(
Icons.sos,
color: AppColor.redColor,
size: 35,
),
trailing: const Icon(Icons.arrow_forward_ios),
subtitle: Text(controller.prfoileData['sosPhone']
.toString()),
onTap: () async {
await controller.updatField(
'sosPhone', TextInputType.phone);
box.write(BoxName.sosPhonePassenger,
controller.prfoileData['sosPhone']);
},
),
// const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
title: 'Sign Out'.tr,
onPressed: () {
LogOutController().logOutPassenger();
}),
GetBuilder<LogOutController>(
builder: (logOutController) {
return MyElevatedButton(
title: 'Delete My Account'.tr,
onPressed: () {
Get.defaultDialog(
title:
'Are you sure to delete your account?'
.tr,
content: Form(
key: logOutController.formKey1,
child: MyTextForm(
controller: logOutController
.emailTextController,
label: 'Type your Email'.tr,
hint: 'Type your Email'.tr,
type:
TextInputType.emailAddress,
),
),
confirm: MyElevatedButton(
title: 'Delete My Account'.tr,
kolor: AppColor.redColor,
onPressed: () async {
await logOutController
.deletePassengerAccount();
}),
cancel: MyElevatedButton(
title: 'No I want'.tr,
onPressed: () {
logOutController
.emailTextController
.clear();
logOutController.update();
Get.back();
}));
});
}),
],
),
],
),
),
),
)),
],
);
}
}
class GenderPicker extends StatelessWidget {
final ProfileController controller = Get.put(ProfileController());
final List<String> genderOptions = ['Male'.tr, 'Female'.tr, 'Other'.tr];
GenderPicker({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 100,
child: CupertinoPicker(
itemExtent: 32.0,
onSelectedItemChanged: (int index) {
controller.setGender(genderOptions[index]);
},
children: genderOptions.map((String value) {
return Text(value);
}).toList(),
),
);
}
}
class EducationDegreePicker extends StatelessWidget {
final ProfileController controller = Get.put(ProfileController());
final List<String> degreeOptions = [
'High School Diploma'.tr,
'Associate Degree'.tr,
'Bachelor\'s Degree'.tr,
'Master\'s Degree'.tr,
'Doctoral Degree'.tr,
];
EducationDegreePicker({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: CupertinoPicker(
// backgroundColor: AppColor.accentColor,
// looping: true,
squeeze: 2,
// diameterRatio: 5,
itemExtent: 32,
onSelectedItemChanged: (int index) {
controller.setDegree(degreeOptions[index]);
},
children: degreeOptions.map((String value) {
return Text(value);
}).toList(),
),
);
}
}
+574
View File
@@ -0,0 +1,574 @@
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';
// الصفحة الرئيسية الجديدة
class ProfileCaptain extends StatelessWidget {
const ProfileCaptain({super.key});
@override
Widget build(BuildContext context) {
// Get.put() يجب أن يكون في مكان يتم استدعاؤه مرة واحدة فقط،
// لكن سنبقيه هنا حسب الكود الأصلي
final controller = Get.put(CaptainProfileController());
return MyScafolld(
title: 'My Profile'.tr,
isleading: true,
body: [
GetBuilder<CaptainProfileController>(
builder: (controller) {
if (controller.isLoading) {
return const Center(child: MyCircularProgressIndicator());
}
if (controller.captainProfileData.isEmpty) {
return Center(child: Text('Failed to load profile data.'.tr));
}
return SingleChildScrollView(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Column(
children: [
// 1. رأس الصفحة: صورة واسم وتقييم
ProfileHeader(
name:
'${controller.captainProfileData['first_name'] ?? ''} ${controller.captainProfileData['last_name'] ?? ''}',
rating:
controller.captainProfileData['ratingDriver'] != null
? double.tryParse(controller
.captainProfileData['ratingDriver']
.toString()) ??
0.0
: 0.0,
ratingCount: int.tryParse(controller
.captainProfileData['ratingCount']
.toString()) ??
0,
),
const SizedBox(height: 24),
// 2. قسم الإجراءات السريعة
ActionsGrid(),
const SizedBox(height: 24),
// 3. بطاقة المعلومات الشخصية
PersonalInfoCard(
data: controller.captainProfileData
.cast<String, dynamic>()),
const SizedBox(height: 16),
// 4. بطاقة معلومات المركبة (قابلة للتوسيع)
VehicleInfoCard(
data: controller.captainProfileData
.cast<String, dynamic>()),
],
),
);
},
),
],
);
}
}
// --- الويدجتس الجديدة المنفصلة لتحسين التصميم ---
/// 1. ويدجت رأس الصفحة
class ProfileHeader extends StatelessWidget {
final String name;
final double rating;
final int ratingCount;
const ProfileHeader({
super.key,
required this.name,
required this.rating,
required this.ratingCount,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
CircleAvatar(
radius: 50,
backgroundColor: Get.theme.primaryColor.withOpacity(0.1),
child: Icon(Icons.person, size: 60, color: Get.theme.primaryColor),
),
const SizedBox(height: 12),
Text(
name,
style: Get.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.star, color: Colors.amber, size: 20),
const SizedBox(width: 4),
Text(
'${rating.toStringAsFixed(1)} (${'reviews'.tr} $ratingCount)',
style: Get.textTheme.titleMedium
?.copyWith(color: Colors.grey.shade600),
),
],
),
],
);
}
}
/// 2. ويدجت شبكة الأزرار
class ActionsGrid extends StatelessWidget {
const ActionsGrid({super.key});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 2.5, // للتحكم في ارتفاع الأزرار
children: [
// _ActionTile(
// title: 'My Cars'.tr,
// icon: Icons.directions_car_filled,
// onTap: () => Get.to(() => CaptainsCars()),
// ),
// _ActionTile(
// title: 'Criminal Record'.tr,
// icon: Icons.description,
// onTap: () => Get.to(() => CriminalDocumemtPage()),
// ),
_ActionTile(
title: 'ShamCash Account'.tr, // غيرت الاسم ليكون أوضح
icon: Icons.account_balance_wallet_rounded, // أيقونة محفظة
// trailing: Icon(Icons.arrow_forward_ios,
// size: 16, color: Colors.grey), // سهم صغير للجمالية
onTap: () {
// استدعاء دالة فتح النافذة
showShamCashInput();
},
),
_ActionTile(
title: 'Behavior Page'.tr,
icon: Icons.checklist_rtl,
onTap: () => Get.to(() => BehaviorPage()),
),
],
);
}
}
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;
final IconData icon;
final VoidCallback onTap;
const _ActionTile(
{required this.title, required this.icon, required this.onTap});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: Get.theme.primaryColor, size: 20),
const SizedBox(width: 8),
Flexible(
child: Text(
title,
style: Get.textTheme.labelLarge,
textAlign: TextAlign.center,
)),
],
),
),
),
);
}
}
/// 3. بطاقة المعلومات الشخصية
class PersonalInfoCard extends StatelessWidget {
final Map<String, dynamic> data;
PersonalInfoCard({super.key, required this.data});
final controller = Get.find<CaptainProfileController>();
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Personal Information'.tr, style: Get.textTheme.titleLarge),
const Divider(height: 24),
_InfoRow(
icon: Icons.phone,
label: 'Phone Number'.tr,
value: data['phone'] ?? ''),
if (data['email'] != null &&
data['email'].toString().contains('intaleqapp')) ...[
TextFormField(
controller: controller.emailController,
keyboardType:
TextInputType.emailAddress, // ✅ لوحة مفاتيح خاصة بالإيميل
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email'.tr,
hintText: 'Enter your email'.tr,
prefixIcon: Icon(Icons.email),
),
autofillHints: [
AutofillHints.email
], // اختياري لتحسين تجربة المستخدم
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () async {
await controller.updateEmail();
},
child: Text('Update'.tr),
),
] else
_InfoRow(
icon: Icons.email,
label: 'Email'.tr,
value: data['email'] ?? '',
),
_InfoRow(
icon: Icons.cake,
label: 'Age'.tr,
value: data['age']?.toString() ?? 'N/A'),
_InfoRow(
icon: Icons.wc,
label: 'Gender'.tr,
value: data['gender'] ?? 'N/A'),
],
),
),
);
}
}
/// 4. بطاقة معلومات المركبة
class VehicleInfoCard extends StatelessWidget {
final Map<String, dynamic> data;
const VehicleInfoCard({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ExpansionTile(
title: Text('Vehicle Details'.tr, style: Get.textTheme.titleLarge),
leading: Icon(Icons.directions_car, color: Get.theme.primaryColor),
childrenPadding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
children: [
_InfoRow(
icon: Icons.branding_watermark,
label: 'Make'.tr,
value: data['make'] ?? ''),
_InfoRow(
icon: Icons.category,
label: 'Model'.tr,
value: data['model'] ?? ''),
_InfoRow(
icon: Icons.palette,
label: 'Color'.tr,
value: data['color'] ?? ''),
_InfoRow(
icon: Icons.pin,
label: 'Plate Number'.tr,
value: data['car_plate'] ?? ''),
_InfoRow(
icon: Icons.confirmation_number,
label: 'VIN'.tr,
value: data['vin'] ?? ''),
_InfoRow(
icon: Icons.event,
label: 'Expiration Date'.tr,
value: data['expiration_date'] ?? ''),
],
),
);
}
}
/// ويدجت لعرض سطر معلومة (أيقونة + عنوان + قيمة) لتجنب التكرار
class _InfoRow extends StatelessWidget {
final IconData icon;
final String label;
final String value;
const _InfoRow(
{required this.icon, required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(icon, color: Colors.grey.shade500, size: 20),
const SizedBox(width: 16),
Text(label, style: Get.textTheme.bodyLarge),
const Spacer(),
Flexible(
child: Text(
value,
style: Get.textTheme.bodyLarge?.copyWith(
color: Colors.grey.shade700, fontWeight: FontWeight.w500),
textAlign: TextAlign.end,
),
),
],
),
);
}
}
@@ -0,0 +1,112 @@
import 'package:animated_text_kit/animated_text_kit.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/profile/promos_controller.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../widgets/mycircular.dart';
class PromosPassengerPage extends StatelessWidget {
const PromosPassengerPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(PromosController());
return MyScafolld(
title: 'Promos For today'.tr,
isleading: true,
body: [
GetBuilder<PromosController>(
builder: (orderHistoryController) => orderHistoryController.isLoading
? const MyCircularProgressIndicator()
: ListView.builder(
itemCount: orderHistoryController.promoList.length,
itemBuilder: (BuildContext context, int index) {
final rides = orderHistoryController.promoList[index];
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: AppColor.secondaryColor,
boxShadow: [
BoxShadow(
color: AppColor.accentColor,
offset: Offset(-3, -3),
blurRadius: 0,
spreadRadius: 0,
blurStyle: BlurStyle.outer),
BoxShadow(
color: AppColor.accentColor,
offset: Offset(3, 3),
blurRadius: 0,
spreadRadius: 0,
blurStyle: BlurStyle.outer)
]),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
AnimatedTextKit(
animatedTexts: [
ScaleAnimatedText(rides['promo_code'],
textStyle: AppStyle.title),
WavyAnimatedText(rides['promo_code'],
textStyle: AppStyle.title),
FlickerAnimatedText(
rides['promo_code'],
textStyle: AppStyle.title),
WavyAnimatedText(rides['promo_code'],
textStyle: AppStyle.title),
],
isRepeatingAnimation: true,
onTap: () {},
),
Text(
rides['description'],
style: AppStyle.title,
),
],
),
Column(
children: [
Text(
rides['validity_start_date'],
style: AppStyle.title,
),
Text(
rides['validity_end_date'],
style: AppStyle.title,
),
],
),
],
),
Text(
'Copy this Promo to use it in your Ride!'.tr,
textAlign: TextAlign.center,
style: AppStyle.headTitle2
.copyWith(color: AppColor.accentColor),
)
],
),
),
),
);
},
),
)
],
);
}
}
+88
View File
@@ -0,0 +1,88 @@
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_driver/constant/style.dart';
import 'package:sefer_driver/views/widgets/my_scafold.dart';
class TaarifPage extends StatelessWidget {
const TaarifPage({super.key});
@override
Widget build(BuildContext context) {
return MyScafolld(isleading: true, title: 'Tariffs'.tr, body: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ListView(
// mainAxisAlignment: MainAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.stretch,
clipBehavior: Clip.hardEdge,
children: [
Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
border: TableBorder.symmetric(),
textBaseline: TextBaseline.alphabetic,
children: [
TableRow(
// decoration: AppStyle.boxDecoration,
children: [
Text('Minimum fare'.tr, style: AppStyle.title),
box.read(BoxName.countryCode) == 'Jordan'
? Text('1 ${'JOD'.tr}', style: AppStyle.title)
: Text('20 ${'LE'.tr}', style: AppStyle.title),
],
),
TableRow(
children: [
Text('Maximum fare'.tr, style: AppStyle.title),
box.read(BoxName.countryCode) == 'Jordan'
? Text('200 ${'JOD'.tr}', style: AppStyle.title)
: Text('15000 ${'LE'.tr}', style: AppStyle.title),
],
),
TableRow(
children: [
Text('Flag-down fee'.tr, style: AppStyle.title),
box.read(BoxName.countryCode) == 'Jordan'
? Text('0.47 ${'JOD'.tr}', style: AppStyle.title)
: Text('15 ${'LE'.tr}', style: AppStyle.title),
],
),
TableRow(
children: [
box.read(BoxName.countryCode) == 'Jordan'
? Text('0.05 ${'JOD'.tr}/min and 0.21 ${'JOD'.tr}/km',
style: AppStyle.title)
: Text('1 ${'LE'.tr}/min and 4 ${'LE'.tr}/km',
style: AppStyle.title),
Text('Including Tax'.tr, style: AppStyle.title),
],
),
],
),
const SizedBox(height: 10),
Text('BookingFee'.tr, style: AppStyle.headTitle2),
const SizedBox(height: 10),
Text('10%', style: AppStyle.title),
const SizedBox(height: 20),
Text('Morning'.tr, style: AppStyle.headTitle2),
const SizedBox(height: 10),
Text(
'from 07:30 till 10:30 (Thursday, Friday, Saturday, Monday)'.tr,
style: AppStyle.title),
const SizedBox(height: 20),
Text('Evening'.tr, style: AppStyle.headTitle2),
const SizedBox(height: 10),
Text(
'from 12:00 till 15:00 (Thursday, Friday, Saturday, Monday)'.tr,
style: AppStyle.title),
const SizedBox(height: 20),
Text('Night'.tr, style: AppStyle.headTitle2),
const SizedBox(height: 10),
Text('from 23:59 till 05:30'.tr, style: AppStyle.title),
],
),
),
]);
}
}