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:
+101
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
+85
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
+342
@@ -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) {}
|
||||
}
|
||||
+67
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+218
@@ -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);
|
||||
}
|
||||
+117
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+392
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user