first commit
This commit is contained in:
101
siro_driver/lib/views/home/Captin/About Us/about_us.dart
Executable file
101
siro_driver/lib/views/home/Captin/About Us/about_us.dart
Executable file
@@ -0,0 +1,101 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_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);
|
||||
}
|
||||
}
|
||||
145
siro_driver/lib/views/home/Captin/About Us/frequantly_question.dart
Executable file
145
siro_driver/lib/views/home/Captin/About Us/frequantly_question.dart
Executable file
@@ -0,0 +1,145 @@
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_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);
|
||||
}
|
||||
}
|
||||
222
siro_driver/lib/views/home/Captin/About Us/settings_captain.dart
Executable file
222
siro_driver/lib/views/home/Captin/About Us/settings_captain.dart
Executable file
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/profile/setting_controller.dart';
|
||||
import 'package:siro_driver/views/lang/languages.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_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.dark_mode_outlined,
|
||||
title: 'App Dark Mode'.tr,
|
||||
subtitle: 'Switch between light and dark themes'.tr,
|
||||
controller: settingsController,
|
||||
valueGetter: (ctrl) => (ctrl).isDarkMode,
|
||||
onChanged: (ctrl) => (ctrl).toggleAppTheme(),
|
||||
),
|
||||
_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.map_outlined,
|
||||
title: 'Map Dark Mode'.tr,
|
||||
subtitle: 'Switch between light and dark map styles'.tr,
|
||||
controller: settingsController,
|
||||
valueGetter: (ctrl) => (ctrl).isMapDarkMode,
|
||||
onChanged: (ctrl) => (ctrl).toggleMapTheme(),
|
||||
),
|
||||
|
||||
_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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
112
siro_driver/lib/views/home/Captin/About Us/using_app_page.dart
Executable file
112
siro_driver/lib/views/home/Captin/About Us/using_app_page.dart
Executable file
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_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
siro_driver/lib/views/home/Captin/About Us/video_page.dart
Executable file
193
siro_driver/lib/views/home/Captin/About Us/video_page.dart
Executable file
@@ -0,0 +1,193 @@
|
||||
import 'package:siro_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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
191
siro_driver/lib/views/home/Captin/assurance_health_page.dart
Executable file
191
siro_driver/lib/views/home/Captin/assurance_health_page.dart
Executable file
@@ -0,0 +1,191 @@
|
||||
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';
|
||||
|
||||
import '../../widgets/my_scafold.dart';
|
||||
|
||||
class AssuranceHealthPage extends StatelessWidget {
|
||||
AssuranceHealthPage({super.key});
|
||||
final AssuranceHealthController assuranceHealthController =
|
||||
Get.put(AssuranceHealthController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return MyScafolld(
|
||||
title: "Health Insurance".tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<AssuranceHealthController>(
|
||||
builder: (controller) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
"When you complete 500 trips, you will be eligible for exclusive health insurance offers."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color:
|
||||
theme.textTheme.bodyMedium?.color?.withOpacity(0.8),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
controller.getTripCountByCaptain(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text("Show My Trip Count".tr),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildTripCountAvatar(
|
||||
controller.tripCount['count'] == null
|
||||
? '0'
|
||||
: controller.tripCount['count'].toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
),
|
||||
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: theme.textTheme.bodyLarge
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
onPressed: () => _showInsuranceDialog(context, controller),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text(
|
||||
"Would you like to proceed with health insurance?".tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showInsuranceDialog(
|
||||
BuildContext context, AssuranceHealthController controller) {
|
||||
final TextEditingController providerController = TextEditingController();
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: Text("Confirmation".tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Would you like to proceed with health insurance?".tr),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: providerController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Do you have a disease for a long time?".tr,
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text("No".tr, style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (providerController.text.isNotEmpty) {
|
||||
await controller.addDriverHealthAssurance(
|
||||
healthInsuranceProvider: providerController.text,
|
||||
);
|
||||
Get.back();
|
||||
} else {
|
||||
Get.snackbar("Error".tr,
|
||||
"Please provide details about any long-term diseases.".tr);
|
||||
}
|
||||
},
|
||||
child: Text("Yes".tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTripCountAvatar(String count) {
|
||||
return Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF42A5F5), Color(0xFF1976D2)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
count,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
111
siro_driver/lib/views/home/Captin/bottom_bar.dart
Executable file
111
siro_driver/lib/views/home/Captin/bottom_bar.dart
Executable file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_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: Theme.of(context).cardColor,
|
||||
currentIndex: controller.currentIndex.value,
|
||||
onTap: controller.changePage,
|
||||
selectedItemColor: AppColor.primaryColor,
|
||||
unselectedItemColor: Theme.of(context).hintColor,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.home),
|
||||
label: 'Home'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.person),
|
||||
label: 'Profile'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
label: 'Statistics'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.account_balance_wallet),
|
||||
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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
302
siro_driver/lib/views/home/Captin/camer_widget.dart
Executable file
302
siro_driver/lib/views/home/Captin/camer_widget.dart
Executable file
@@ -0,0 +1,302 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:siro_driver/constant/colors.dart';
|
||||
// import 'package:siro_driver/constant/style.dart';
|
||||
// import 'package:siro_driver/controller/functions/camer_controller.dart';
|
||||
// import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
// import 'package:siro_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
siro_driver/lib/views/home/Captin/driver_map_page.dart
Executable file
448
siro_driver/lib/views/home/Captin/driver_map_page.dart
Executable file
@@ -0,0 +1,448 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_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>) {
|
||||
// 🔥 [Fix] argumentLoading ضرورية هنا للعودة للرحلة من صفحة الهوم
|
||||
// (عند العودة لا يُستدعى onInit() لأن الكنترولر موجود مسبقاً)
|
||||
// الحماية من التكرار موجودة داخل argumentLoading بواسطة _isRouteRequested flag
|
||||
mapDriverController.argumentLoading();
|
||||
mapDriverController.startTimerToShowPassengerInfoWindowFromDriver();
|
||||
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) {
|
||||
final theme = Theme.of(context);
|
||||
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: theme.cardColor
|
||||
.withOpacity(0.95), // Adaptive background
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5)),
|
||||
],
|
||||
border: Border.all(
|
||||
color: theme.dividerColor.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// أيقونة الاتجاه
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(controller.currentManeuverIcon,
|
||||
color: Colors.white, size: 24),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// نص التعليمات
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${"NEXT STEP".tr} (${controller.distanceToNextStep})",
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
controller.currentInstruction,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
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: Theme.of(context).cardColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).shadowColor.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: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).shadowColor.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: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
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
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
411
siro_driver/lib/views/home/Captin/history/history_captain.dart
Executable file
411
siro_driver/lib/views/home/Captin/history/history_captain.dart
Executable file
@@ -0,0 +1,411 @@
|
||||
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 '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/auth/captin/history_captain.dart';
|
||||
import 'package:siro_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.white,
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
);
|
||||
}
|
||||
|
||||
final List trips = controller.historyData['message'] ?? [];
|
||||
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// 1. Custom App Bar
|
||||
SliverAppBar(
|
||||
expandedHeight: 180,
|
||||
pinned: true,
|
||||
stretch: true,
|
||||
backgroundColor: FinanceDesignSystem.primaryDark,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Ride History'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -50,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
Icons.history_rounded,
|
||||
size: 200,
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
trips.length.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Total Rides'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. Trips List
|
||||
if (trips.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.history_toggle_off_rounded,
|
||||
size: 64,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'No Rides Yet'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Your completed trips will appear here'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final trip = trips[index];
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _TripHistoryCard(
|
||||
trip: trip,
|
||||
onTap: () {
|
||||
if (trip['status'] != 'Cancel' &&
|
||||
trip['status'] != 'CancelByPassenger') {
|
||||
controller
|
||||
.getHistoryDetails(trip['order_id']);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'This Trip Was Cancelled'.tr,
|
||||
'This Trip Was Cancelled'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: trips.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TripHistoryCard extends StatelessWidget {
|
||||
final Map trip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TripHistoryCard({required this.trip, required this.onTap});
|
||||
|
||||
String _formatDateToSyria(String? dateStr) {
|
||||
if (dateStr == null) return '';
|
||||
try {
|
||||
// Parse GMT date
|
||||
DateTime date = DateTime.parse(dateStr);
|
||||
// Add 3 hours for Syria (GMT+3)
|
||||
DateTime syriaDate = date.add(const Duration(hours: 3));
|
||||
// Format to readable string
|
||||
return DateFormat('yyyy-MM-dd HH:mm').format(syriaDate);
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.primaryDark
|
||||
.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.receipt_long_rounded,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${'OrderId'.tr} #${trip['order_id']}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDateToSyria(trip['created_at']),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildStatusChip(trip['status']),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Icon(Icons.circle,
|
||||
size: 12, color: FinanceDesignSystem.accentBlue),
|
||||
Container(
|
||||
width: 2,
|
||||
height: 20,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
Icon(Icons.location_on_rounded,
|
||||
size: 14, color: FinanceDesignSystem.dangerRed),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
trip['start_name'] ?? 'Pickup Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
trip['end_name'] ?? 'Destination Location'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trip['price'] != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${trip['price']} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 12, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusChip(String? status) {
|
||||
Color color;
|
||||
IconData icon;
|
||||
String label = status ?? 'Unknown';
|
||||
List<Color> gradientColors;
|
||||
|
||||
switch (status) {
|
||||
case 'Apply':
|
||||
color = FinanceDesignSystem.successGreen;
|
||||
icon = Icons.check_circle_rounded;
|
||||
label = 'Completed'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'Refused':
|
||||
color = FinanceDesignSystem.dangerRed;
|
||||
icon = Icons.cancel_rounded;
|
||||
label = 'Refused'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'Cancel':
|
||||
color = Colors.orange;
|
||||
icon = Icons.info_rounded;
|
||||
label = 'Cancelled'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
case 'CancelByPassenger':
|
||||
color = const Color(0xFF6B4EFF);
|
||||
icon = Icons.person_off_rounded;
|
||||
label = 'Cancelled by Passenger'.tr;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
break;
|
||||
default:
|
||||
color = Colors.grey;
|
||||
icon = Icons.help_rounded;
|
||||
gradientColors = [color.withOpacity(0.2), color.withOpacity(0.05)];
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: gradientColors,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
371
siro_driver/lib/views/home/Captin/history/history_details_page.dart
Executable file
371
siro_driver/lib/views/home/Captin/history/history_details_page.dart
Executable file
@@ -0,0 +1,371 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import 'package:siro_driver/controller/auth/captin/history_captain.dart';
|
||||
import 'package:siro_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.withValues(alpha: 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: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: {
|
||||
if (startLocation != null && endLocation != null)
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: [startLocation, endLocation],
|
||||
color: Colors.deepPurple,
|
||||
width: 5,
|
||||
),
|
||||
},
|
||||
onMapCreated: (IntaleqMapController 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, left: 60, top: 60, right: 60, bottom: 60));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
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.withValues(alpha: 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,
|
||||
);
|
||||
}
|
||||
407
siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart
Executable file
407
siro_driver/lib/views/home/Captin/home_captain/drawer_captain.dart
Executable file
@@ -0,0 +1,407 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/upload_image.dart';
|
||||
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import 'package:siro_driver/device_compatibility_page.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
// استيراد الصفحات الأخرى... تأكد من صحة المسارات
|
||||
import 'package:siro_driver/views/Rate/rate_app_page.dart';
|
||||
import 'package:siro_driver/views/auth/captin/contact_us_page.dart';
|
||||
import 'package:siro_driver/views/auth/captin/invite_driver_screen.dart';
|
||||
import 'package:siro_driver/views/home/statistics/statistics_dashboard.dart';
|
||||
import 'package:siro_driver/views/gamification/challenges_page.dart';
|
||||
import 'package:siro_driver/views/gamification/leaderboard_page.dart';
|
||||
import 'package:siro_driver/views/gamification/referral_center_page.dart';
|
||||
import 'package:siro_driver/views/home/journal/schedule_page.dart';
|
||||
import 'package:siro_driver/views/notification/available_rides_page.dart';
|
||||
import 'package:siro_driver/views/auth/captin/logout_captain.dart';
|
||||
import 'package:siro_driver/views/home/Captin/history/history_captain.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/help_captain.dart';
|
||||
import 'package:siro_driver/views/home/Captin/About Us/settings_captain.dart';
|
||||
import 'package:siro_driver/views/home/my_wallet/walet_captain.dart';
|
||||
import 'package:siro_driver/views/home/profile/profile_captain.dart';
|
||||
import 'package:siro_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: 'Statistics'.tr,
|
||||
icon: Icons.bar_chart_rounded,
|
||||
color: Colors.deepPurple,
|
||||
onTap: () => Get.to(() => StatisticsDashboard())),
|
||||
DrawerItem(
|
||||
title: 'Challenges'.tr,
|
||||
icon: Icons.bolt_rounded,
|
||||
color: Colors.amber,
|
||||
onTap: () => Get.to(() => ChallengesPage())),
|
||||
DrawerItem(
|
||||
title: 'My Schedule'.tr,
|
||||
icon: Icons.calendar_today_rounded,
|
||||
color: Colors.teal,
|
||||
onTap: () => Get.to(() => SchedulePage())),
|
||||
DrawerItem(
|
||||
title: 'Leaderboard'.tr,
|
||||
icon: Icons.leaderboard_rounded,
|
||||
color: Colors.red,
|
||||
onTap: () => Get.to(() => LeaderboardPage())),
|
||||
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: 'Invite Driver'.tr,
|
||||
icon: Icons.person_add_rounded,
|
||||
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.withValues(alpha: 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: () {
|
||||
Navigator.pop(context); // لإغلاق القائمة عند الضغط بشكل آمن
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
item.onTap(); // الانتقال للصفحة بعد تأخير بسيط لإظهار الأنيميشن
|
||||
});
|
||||
},
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
splashColor: item.color.withValues(alpha: 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.withValues(alpha: 0.3), blurRadius: 5)
|
||||
],
|
||||
),
|
||||
child: controller.isloading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: CircleAvatar(
|
||||
backgroundImage: NetworkImage((box
|
||||
.read(BoxName.driverPhotoUrl)
|
||||
?.toString()
|
||||
.isNotEmpty ==
|
||||
true)
|
||||
? box.read(BoxName.driverPhotoUrl)
|
||||
: '${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: [
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
homeCaptainController.rating.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
const Icon(Icons.star, color: Colors.amber, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Theme.of(context).primaryColor, Colors.blue.shade700],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
189
siro_driver/lib/views/home/Captin/home_captain/driver_call_page.dart
Executable file
189
siro_driver/lib/views/home/Captin/home_captain/driver_call_page.dart
Executable file
@@ -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,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
331
siro_driver/lib/views/home/Captin/home_captain/help_captain.dart
Executable file
331
siro_driver/lib/views/home/Captin/home_captain/help_captain.dart
Executable file
@@ -0,0 +1,331 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/captin/help/help_controller.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/help_details_replay_page.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../widgets/my_scafold.dart';
|
||||
|
||||
class HelpCaptain extends StatelessWidget {
|
||||
HelpCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(HelpController());
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Helping Page'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
),
|
||||
child: Text(
|
||||
'If you need any help or have questions, this is the right place to do that. You are welcome!'
|
||||
.tr,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Submit Your Question'.tr,
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GetBuilder<HelpController>(
|
||||
builder: (helpController) => Form(
|
||||
key: helpController.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: helpController.helpQuestionController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your Question here'.tr,
|
||||
prefixIcon:
|
||||
const Icon(Icons.question_answer_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
maxLines: 3,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your question'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
helpController.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (helpController.formKey.currentState!
|
||||
.validate()) {
|
||||
helpController.addHelpQuestion();
|
||||
helpController.helpQuestionController
|
||||
.clear();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text('Submit Question'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Your Questions'.tr,
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GetBuilder<HelpController>(
|
||||
builder: (helpController) {
|
||||
final questions = helpController.helpQuestionDate['message'];
|
||||
if (questions == null || questions.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text('No questions asked yet.'.tr),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: questions.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
var list = questions[index];
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: theme.dividerColor),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
list['datecreated'],
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
helpController.getIndex(
|
||||
int.parse(EncryptionHelper.instance
|
||||
.decryptData(list['id'])),
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['helpQuestion']));
|
||||
helpController.getHelpRepley(list['id'].toString());
|
||||
Get.to(() => const HelpDetailsReplayPage());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
110
siro_driver/lib/views/home/Captin/home_captain/help_details_replay_page.dart
Executable file
110
siro_driver/lib/views/home/Captin/home_captain/help_details_replay_page.dart
Executable file
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_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()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Question Bubble (Aligned to Start/End based on locale, usually start for sender)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: Get.width * 0.75),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your Question'.tr,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
helpController.qustion,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Reply Bubble (Aligned to opposite side)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: Get.width * 0.75),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Support Reply'.tr,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Builder(builder: (context) {
|
||||
final replayData = helpController.helpQuestionRepleyDate['message'];
|
||||
final isNoReply = helpController.status == 'not yet' ||
|
||||
replayData == null ||
|
||||
EncryptionHelper.instance.decryptData(replayData['replay']).toString() == 'not yet';
|
||||
|
||||
return Text(
|
||||
isNoReply
|
||||
? 'No Response yet.'.tr
|
||||
: EncryptionHelper.instance.decryptData(replayData['replay']).toString(),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
],
|
||||
isleading: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
672
siro_driver/lib/views/home/Captin/home_captain/home_captin.dart
Executable file
672
siro_driver/lib/views/home/Captin/home_captain/home_captin.dart
Executable file
@@ -0,0 +1,672 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bubble_head/bubble.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
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:siro_driver/views/home/Captin/home_captain/drawer_captain.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../constant/info.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 '../../../../controller/home/navigation/navigation_view.dart';
|
||||
import '../../../../controller/profile/setting_controller.dart';
|
||||
import '../../../../env/env.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../notification/available_rides_page.dart';
|
||||
import '../driver_map_page.dart';
|
||||
import 'widget/connect.dart';
|
||||
import 'widget/left_menu_map_captain.dart';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Design Tokens (Responsive to Theme)
|
||||
// ─────────────────────────────────────────────
|
||||
class _Token {
|
||||
static Color surface(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF1A1A2E)
|
||||
: Colors.white;
|
||||
|
||||
static Color surfaceCard(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF16213E)
|
||||
: const Color(0xFFF8F9FA);
|
||||
|
||||
static const Color accent = Color(0xFFF0A500);
|
||||
static Color accentSoft(BuildContext context) =>
|
||||
const Color(0xFFF0A500).withOpacity(0.12);
|
||||
static const Color danger = Color(0xFFE53935);
|
||||
static const Color success = Color(0xFF2ECC71);
|
||||
static const Color info = Color(0xFF3498DB);
|
||||
|
||||
static Color border(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF2A2A4A)
|
||||
: Colors.grey.withOpacity(0.2);
|
||||
|
||||
static Color text(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: const Color(0xFF2D3436);
|
||||
|
||||
static Color textDim(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white38
|
||||
: Colors.black45;
|
||||
|
||||
static const double radius = 16.0;
|
||||
static const double radiusSm = 10.0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Root Widget
|
||||
// ─────────────────────────────────────────────
|
||||
class HomeCaptain extends StatelessWidget {
|
||||
HomeCaptain({super.key});
|
||||
|
||||
final LocationController locationController =
|
||||
Get.put(LocationController(), permanent: true);
|
||||
final HomeCaptainController homeCaptainController =
|
||||
Get.put(HomeCaptainController(), permanent: true);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await closeOverlayIfFound();
|
||||
if (!context.mounted) return;
|
||||
await checkForUpdate(context);
|
||||
if (!context.mounted) return;
|
||||
await getPermissionOverlay();
|
||||
if (!context.mounted) return;
|
||||
await showDriverGiftClaim(context);
|
||||
if (!context.mounted) return;
|
||||
await checkForAppliedRide(context);
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: _Token.surface(context),
|
||||
appBar: const _HomeAppBar(),
|
||||
drawer: AppDrawer(),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
const _MapView(),
|
||||
leftMainMenuCaptainIcons(),
|
||||
const _BottomStatusBar(),
|
||||
const _FloatingControls(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// AppBar — no blur, solid gradient
|
||||
// ─────────────────────────────────────────────
|
||||
class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const _HomeAppBar();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ctrl = Get.find<HomeCaptainController>();
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: _Token.surface(context),
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
titleSpacing: 0,
|
||||
// ── Logo + App Name ──────────────────────
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
_LogoBadge(),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
AppInformation.appName.split(' ')[0].tr,
|
||||
style: const TextStyle(
|
||||
color: _Token.accent,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// ── Refuse Counter ───────────────────
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _PillBadge(
|
||||
icon: Icons.block_rounded,
|
||||
label: c.countRefuse.toString(),
|
||||
color: _Token.danger,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// ── Map Controls Row ─────────────────
|
||||
_AppBarControls(ctrl: ctrl),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogoBadge extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: _Token.accent, width: 1.5),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset('assets/images/logo.gif', fit: BoxFit.cover),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _PillBadge extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final Color color;
|
||||
const _PillBadge(
|
||||
{required this.icon, required this.label, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.35)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 13),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color, fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _AppBarControls extends StatelessWidget {
|
||||
final HomeCaptainController ctrl;
|
||||
const _AppBarControls({required this.ctrl});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
borderRadius: BorderRadius.circular(_Token.radiusSm),
|
||||
border: Border.all(color: _Token.border(context)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Navigation
|
||||
_IconBtn(
|
||||
icon: Icons.map_rounded,
|
||||
color: _Token.success,
|
||||
tooltip: 'Navigation'.tr,
|
||||
onTap: () => Get.to(() => const NavigationView()),
|
||||
),
|
||||
// Heatmap
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _IconBtn(
|
||||
icon: Icons.local_fire_department_rounded,
|
||||
color:
|
||||
c.isHeatmapVisible ? Colors.orange : Colors.grey.shade600,
|
||||
tooltip: 'Heatmap'.tr,
|
||||
onTap: c.toggleHeatmap,
|
||||
),
|
||||
),
|
||||
// Center on me
|
||||
_IconBtn(
|
||||
icon: Icons.my_location_rounded,
|
||||
color: _Token.accent,
|
||||
tooltip: 'My Location'.tr,
|
||||
onTap: () {
|
||||
ctrl.mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(
|
||||
Get.find<LocationController>().myLocation,
|
||||
17.5,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _IconBtn extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
const _IconBtn(
|
||||
{required this.icon,
|
||||
required this.color,
|
||||
required this.tooltip,
|
||||
required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Map View
|
||||
// ─────────────────────────────────────────────
|
||||
class _MapView extends StatelessWidget {
|
||||
const _MapView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<HomeCaptainController>(
|
||||
builder: (ctrl) {
|
||||
if (ctrl.isLoading) return const MyCircularProgressIndicator();
|
||||
|
||||
return GetBuilder<SettingController>(
|
||||
builder: (s) => GetBuilder<LocationController>(
|
||||
builder: (loc) => IntaleqMap(
|
||||
apiKey: Env.mapSaasKey,
|
||||
onMapCreated: ctrl.onMapCreated,
|
||||
minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: (loc.myLocation.latitude == 0 ||
|
||||
loc.myLocation.latitude.isNaN)
|
||||
? ctrl.myLocation
|
||||
: loc.myLocation,
|
||||
zoom: 15,
|
||||
),
|
||||
mapType:
|
||||
s.isMapDarkMode ? IntaleqMapType.normal : IntaleqMapType.light,
|
||||
polygons: ctrl.heatmapPolygons,
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: MarkerId('MyLocation'.tr),
|
||||
position: loc.myLocation,
|
||||
rotation: loc.heading,
|
||||
flat: true,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
icon: ctrl.carIcon,
|
||||
)
|
||||
},
|
||||
myLocationButtonEnabled: false,
|
||||
myLocationEnabled: false,
|
||||
compassEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Bottom Status Bar — no blur, solid card
|
||||
// ─────────────────────────────────────────────
|
||||
class _BottomStatusBar extends StatelessWidget {
|
||||
const _BottomStatusBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
right: 12,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showDetailsSheet(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
borderRadius: BorderRadius.circular(_Token.radius),
|
||||
border: Border.all(color: _Token.border(context)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.35),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Online toggle
|
||||
const ConnectWidget(),
|
||||
const Spacer(),
|
||||
// Ride count
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _StatChip(
|
||||
icon: Icons.directions_car_rounded,
|
||||
value: c.countRideToday,
|
||||
label: 'Rides'.tr,
|
||||
color: _Token.info,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// Today earnings
|
||||
GetBuilder<HomeCaptainController>(
|
||||
builder: (c) => _StatChip(
|
||||
icon: Entypo.wallet,
|
||||
value: c.totalMoneyToday.toString(),
|
||||
label: 'Today'.tr,
|
||||
color: _Token.success,
|
||||
),
|
||||
),
|
||||
// Chevron hint
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.keyboard_arrow_up_rounded,
|
||||
color: Colors.grey, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDetailsSheet(BuildContext context) {
|
||||
final ctrl = Get.find<HomeCaptainController>();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _DetailsSheet(ctrl),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String value;
|
||||
final String label;
|
||||
final Color color;
|
||||
const _StatChip(
|
||||
{required this.icon,
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(label,
|
||||
style: const TextStyle(color: Colors.white38, fontSize: 11)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Details Bottom Sheet — replaces AlertDialog
|
||||
// ─────────────────────────────────────────────
|
||||
class _DetailsSheet extends StatelessWidget {
|
||||
final HomeCaptainController ctrl;
|
||||
const _DetailsSheet(this.ctrl);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.surfaceCard(context),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Drag handle
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _Token.border(context),
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Your Activity'.tr,
|
||||
style: TextStyle(
|
||||
color: _Token.text(context),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(color: _Token.border(context), height: 1),
|
||||
const SizedBox(height: 16),
|
||||
_SheetRow(
|
||||
icon: Entypo.wallet,
|
||||
color: _Token.success,
|
||||
label: 'Today'.tr,
|
||||
value: ctrl.totalMoneyToday.toString(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Entypo.wallet,
|
||||
color: _Token.accent,
|
||||
label: AppInformation.appName,
|
||||
value: ctrl.totalMoneyInSEFER.toString(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Divider(color: _Token.border(context), height: 1),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Icons.timer_outlined,
|
||||
color: _Token.success,
|
||||
label: 'Active Duration'.tr,
|
||||
value: ctrl.stringActiveDuration,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Icons.access_time_rounded,
|
||||
color: _Token.info,
|
||||
label: 'Total Connection'.tr,
|
||||
value: ctrl.totalDurationToday,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Divider(color: _Token.border(context), height: 1),
|
||||
const SizedBox(height: 12),
|
||||
_SheetRow(
|
||||
icon: Icons.star_rounded,
|
||||
color: _Token.accent,
|
||||
label: 'Total Points'.tr,
|
||||
value: ctrl.totalPoints.toString(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: _Token.accentSoft(context),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(_Token.radiusSm)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text('Close'.tr,
|
||||
style: const TextStyle(
|
||||
color: _Token.accent, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SheetRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String label;
|
||||
final String value;
|
||||
const _SheetRow(
|
||||
{required this.icon,
|
||||
required this.color,
|
||||
required this.label,
|
||||
required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(label,
|
||||
style: TextStyle(color: _Token.textDim(context), fontSize: 14)),
|
||||
const Spacer(),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
color: color, fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Floating Action Buttons (right side)
|
||||
// ─────────────────────────────────────────────
|
||||
class _FloatingControls extends StatelessWidget {
|
||||
const _FloatingControls({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
bottom: 88,
|
||||
right: 12,
|
||||
child: GetBuilder<HomeCaptainController>(
|
||||
builder: (ctrl) => Column(
|
||||
children: [
|
||||
// Bubble/overlay mode (Android only)
|
||||
if (Platform.isAndroid) ...[
|
||||
_Fab(
|
||||
onTap: () =>
|
||||
Bubble().startBubbleHead(sendAppToBackground: true),
|
||||
tooltip: 'Overlay'.tr,
|
||||
child: Image.asset('assets/images/logo1.png',
|
||||
width: 26, height: 26),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
// Available rides
|
||||
_Fab(
|
||||
onTap: () => Get.to(() => const AvailableRidesPage()),
|
||||
icon: Icons.list_alt_rounded,
|
||||
color: AppColor.primaryColor,
|
||||
tooltip: 'Available Rides'.tr,
|
||||
),
|
||||
// Continue active ride
|
||||
if (box.read(BoxName.rideStatus) == 'Applied' ||
|
||||
box.read(BoxName.rideStatus) == 'Begin') ...[
|
||||
const SizedBox(height: 10),
|
||||
_Fab(
|
||||
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();
|
||||
}
|
||||
},
|
||||
icon: Icons.navigation_rounded,
|
||||
color: _Token.info,
|
||||
tooltip: 'Continue Ride'.tr,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Fab extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
final IconData? icon;
|
||||
final Widget? child;
|
||||
final Color? color;
|
||||
final String? tooltip;
|
||||
const _Fab(
|
||||
{required this.onTap, this.icon, this.child, this.color, this.tooltip});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
message: tooltip ?? '',
|
||||
child: Material(
|
||||
color: color ?? _Token.surfaceCard(context),
|
||||
shape: const CircleBorder(),
|
||||
elevation: 6,
|
||||
shadowColor: (color ?? Colors.black).withOpacity(0.4),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
customBorder: const CircleBorder(),
|
||||
child: SizedBox(
|
||||
width: 52,
|
||||
height: 52,
|
||||
child: Center(
|
||||
child: child ??
|
||||
Icon(icon,
|
||||
color: color != null ? Colors.white : _Token.accent,
|
||||
size: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Helper
|
||||
// ─────────────────────────────────────────────
|
||||
Future<void> checkForAppliedRide(BuildContext context) async {
|
||||
checkForPendingOrderFromServer();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../../constant/api_key.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||
|
||||
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>();
|
||||
|
||||
return Obx(() {
|
||||
final LatLng currentLocation = LatLng(
|
||||
locationController.myLocation.latitude,
|
||||
locationController.myLocation.longitude);
|
||||
final double currentHeading = locationController.heading;
|
||||
|
||||
return IntaleqMap(
|
||||
apiKey: AK.mapSaasKey,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: currentLocation,
|
||||
zoom: 15,
|
||||
bearing: currentHeading,
|
||||
),
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: const MarkerId('myLocation'),
|
||||
position: currentLocation,
|
||||
rotation: currentHeading,
|
||||
icon: homeCaptainController.carIcon,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 2,
|
||||
)
|
||||
},
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
// You can assign this controller if needed
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
84
siro_driver/lib/views/home/Captin/home_captain/widget/call_page.dart
Executable file
84
siro_driver/lib/views/home/Captin/home_captain/widget/call_page.dart
Executable file
@@ -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
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
151
siro_driver/lib/views/home/Captin/home_captain/widget/connect.dart
Executable file
151
siro_driver/lib/views/home/Captin/home_captain/widget/connect.dart
Executable file
@@ -0,0 +1,151 @@
|
||||
import 'package:siro_driver/controller/functions/tts.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
|
||||
import '../../../../../constant/style.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(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: homeCaptainController.isActive
|
||||
? [const Color(0xFF00C853), const Color(0xFF00E676)]
|
||||
: [Colors.grey.shade600, Colors.grey.shade400],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (homeCaptainController.isActive
|
||||
? const Color(0xFF00C853)
|
||||
: Colors.grey)
|
||||
.withOpacity(0.4),
|
||||
spreadRadius: 0,
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CupertinoButton(
|
||||
onPressed: homeCaptainController.onButtonSelected,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
homeCaptainController.isActive
|
||||
? Icons.power_settings_new_rounded
|
||||
: Icons.power_off_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
homeCaptainController.isActive
|
||||
? 'Online'.tr
|
||||
: 'Offline'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
323
siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart
Executable file
323
siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart
Executable file
@@ -0,0 +1,323 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/home/Captin/driver_map_page.dart';
|
||||
import 'package:siro_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
|
||||
import 'package:siro_driver/views/auth/syria/registration_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
import '../../../../../constant/links.dart';
|
||||
import '../../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../../controller/functions/crud.dart';
|
||||
import '../../../../../controller/home/captin/order_request_controller.dart';
|
||||
import '../../../../../print.dart';
|
||||
import '../../../../Rate/ride_calculate_driver.dart';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Design Tokens (Responsive)
|
||||
// ─────────────────────────────────────────────
|
||||
class _T {
|
||||
static Color surface(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF16213E)
|
||||
: Colors.white;
|
||||
|
||||
static const Color accent = Color(0xFFF0A500);
|
||||
static const Color blue = Color(0xFF3498DB);
|
||||
|
||||
static Color border(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF2A2A4A)
|
||||
: Colors.grey.withOpacity(0.2);
|
||||
|
||||
static const double radius = 14.0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Left Side Menu
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// Returns the vertical icon column anchored to the left-center of the map.
|
||||
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||
return GetBuilder<HomeCaptainController>(
|
||||
builder: (ctrl) => Positioned(
|
||||
// Place just above the bottom status bar
|
||||
bottom: 100,
|
||||
left: 10,
|
||||
child: Builder(builder: (context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _T.surface(context),
|
||||
borderRadius: BorderRadius.circular(_T.radius),
|
||||
border: Border.all(color: _T.border(context)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(2, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ── 1. Active Ride shortcut ──────────
|
||||
_MenuIcon(
|
||||
icon: Icons.directions_car_rounded,
|
||||
color:
|
||||
box.read(BoxName.rideArgumentsFromBackground) == 'failure'
|
||||
? Colors.red.shade400
|
||||
: Colors.green.shade400,
|
||||
tooltip: 'Active Ride'.tr,
|
||||
onTap: () async {
|
||||
await checkForPendingOrderFromServer();
|
||||
if (box.read(BoxName.rideArgumentsFromBackground) !=
|
||||
'failure') {
|
||||
Get.to(
|
||||
() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArgumentsFromBackground),
|
||||
);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Ride info'.tr,
|
||||
'you dont have accepted ride'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
box.write(BoxName.rideArgumentsFromBackground, 'failure');
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
box.write(BoxName.rideStatus, 'no_ride');
|
||||
Log.print(box.read(BoxName.statusDriverLocation));
|
||||
ctrl.update();
|
||||
},
|
||||
),
|
||||
|
||||
_Divider(context),
|
||||
|
||||
// ── 2. Earnings Chart ────────────────
|
||||
_MenuIcon(
|
||||
icon: FontAwesome5.chart_bar,
|
||||
color: _T.blue,
|
||||
tooltip: 'Earnings'.tr,
|
||||
onTap: () {
|
||||
final now = DateTime.now();
|
||||
final lastTimeRaw = box.read(BoxName.lastTimeStaticThrottle);
|
||||
DateTime? lastTime;
|
||||
|
||||
if (lastTimeRaw != null) {
|
||||
try {
|
||||
lastTime = DateTime.parse(lastTimeRaw.toString());
|
||||
} catch (_) {
|
||||
lastTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastTime == null ||
|
||||
now.difference(lastTime).inMinutes >= 2) {
|
||||
box.write(
|
||||
BoxName.lastTimeStaticThrottle, now.toIso8601String());
|
||||
Get.to(() => RideCalculateDriver());
|
||||
} else {
|
||||
final left = 2 - now.difference(lastTime).inMinutes;
|
||||
NotificationController().showNotification(
|
||||
'Intaleq Driver'.tr,
|
||||
'${'Please wait'.tr} $left ${"minutes before trying again.".tr}',
|
||||
'ding',
|
||||
'',
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () =>
|
||||
box.write(BoxName.statusDriverLocation, 'off'),
|
||||
),
|
||||
|
||||
// ── 3. VIP Orders (2023+ cars only) ──
|
||||
if (int.tryParse(box.read(BoxName.carYear).toString()) != null &&
|
||||
int.parse(box.read(BoxName.carYear).toString()) > 2023) ...[
|
||||
_Divider(context),
|
||||
_MenuIcon(
|
||||
icon: Octicons.watch,
|
||||
color: _T.accent,
|
||||
tooltip: 'VIP Orders'.tr,
|
||||
onTap: () => Get.to(() => const VipOrderPage()),
|
||||
),
|
||||
],
|
||||
|
||||
// _Divider(context),
|
||||
|
||||
// // ── 4. Driver Registration ──────────
|
||||
// _MenuIcon(
|
||||
// icon: Icons.person_add_alt_1_rounded,
|
||||
// color: Colors.purple.shade400,
|
||||
// tooltip: 'Registration'.tr,
|
||||
// onTap: () => Get.to(() => const RegistrationView()),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Reusable sub-widgets
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
class _MenuIcon extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String tooltip;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
const _MenuIcon({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.tooltip,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
child: Icon(icon, color: color, size: 26),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Thin separator between icons
|
||||
class _Divider extends StatelessWidget {
|
||||
final BuildContext context;
|
||||
const _Divider(this.context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
height: 1,
|
||||
width: 32,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
color: _T.border(this.context),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Server Helpers (unchanged logic)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
Future<void> checkForPendingOrderFromServer() async {
|
||||
bool isProcessing = false;
|
||||
if (isProcessing) return;
|
||||
|
||||
final driverId = box.read(BoxName.driverID)?.toString();
|
||||
if (driverId == null) return;
|
||||
|
||||
isProcessing = true;
|
||||
|
||||
try {
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.getArgumentAfterAppliedFromBackground,
|
||||
payload: {'driver_id': driverId},
|
||||
);
|
||||
Log.print('response: $response');
|
||||
|
||||
if (response['status'] == 'success') {
|
||||
final Map<String, dynamic> orderInfo = response['message'];
|
||||
final Map<String, dynamic> rideArgs =
|
||||
_transformServerDataToAppArguments(orderInfo);
|
||||
|
||||
final customerToken = response['message']['token_passenger'];
|
||||
final orderId = response['message']['ride_id'].toString();
|
||||
|
||||
box.write(BoxName.rideArgumentsFromBackground, rideArgs);
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
box.write(BoxName.rideStatus, 'Apply');
|
||||
Get.put(OrderRequestController()).changeApplied();
|
||||
|
||||
Get.to(
|
||||
() => PassengerLocationMapPage(),
|
||||
arguments: box.read(BoxName.rideArgumentsFromBackground),
|
||||
);
|
||||
} else {
|
||||
box.write(BoxName.rideArgumentsFromBackground, 'failure');
|
||||
}
|
||||
} catch (_) {
|
||||
// silent
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _transformServerDataToAppArguments(
|
||||
Map<String, dynamic> d) {
|
||||
String s(String key, [String def = 'unknown']) => d[key]?.toString() ?? def;
|
||||
|
||||
return {
|
||||
'passengerLocation': s('passenger_location'),
|
||||
'passengerDestination': s('passenger_destination'),
|
||||
'Duration': s('duration'),
|
||||
'totalCost': s('total_cost'),
|
||||
'Distance': s('distance'),
|
||||
'name': s('name'),
|
||||
'phone': s('phone'),
|
||||
'email': s('email'),
|
||||
'tokenPassenger': s('token_passenger'),
|
||||
'direction': s('direction_url'),
|
||||
'DurationToPassenger': s('duration_to_passenger'),
|
||||
'rideId': s('ride_id'),
|
||||
'passengerId': s('passenger_id'),
|
||||
'driverId': s('driver_id'),
|
||||
'durationOfRideValue': s('duration_of_ride'),
|
||||
'paymentAmount': s('payment_amount'),
|
||||
'paymentMethod': s('payment_method'),
|
||||
'passengerWalletBurc': s('passenger_wallet_burc'),
|
||||
'timeOfOrder': s('time_of_order'),
|
||||
'totalPassenger': s('total_passenger'),
|
||||
'carType': s('car_type'),
|
||||
'kazan': s('kazan'),
|
||||
'startNameLocation': s('start_name_location'),
|
||||
'endNameLocation': s('end_name_location'),
|
||||
'step0': s('step0'),
|
||||
'step1': s('step1'),
|
||||
'step2': s('step2'),
|
||||
'step3': s('step3'),
|
||||
'step4': s('step4'),
|
||||
'WalletChecked': (d['wallet_checked'] == 1).toString(),
|
||||
'isHaveSteps': (d['has_steps'] == 1) ? 'startEnd' : 'noSteps',
|
||||
};
|
||||
}
|
||||
|
||||
void _sendAcceptanceNotification(String? customerToken, dynamic rideId) {
|
||||
if (customerToken == null || customerToken.isEmpty) return;
|
||||
|
||||
List<String> body = [
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
box.read(BoxName.tokenDriver).toString(),
|
||||
rideId.toString(),
|
||||
];
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: customerToken,
|
||||
title: 'Accepted Ride'.tr,
|
||||
body: 'your ride is Accepted'.tr,
|
||||
isTopic: false,
|
||||
tone: 'start',
|
||||
driverList: body,
|
||||
category: 'Accepted Ride',
|
||||
);
|
||||
}
|
||||
67
siro_driver/lib/views/home/Captin/home_captain/widget/zones_controller.dart
Executable file
67
siro_driver/lib/views/home/Captin/home_captain/widget/zones_controller.dart
Executable file
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.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);
|
||||
}
|
||||
}
|
||||
273
siro_driver/lib/views/home/Captin/maintain_center_page.dart
Executable file
273
siro_driver/lib/views/home/Captin/maintain_center_page.dart
Executable file
@@ -0,0 +1,273 @@
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/home/captin/help/maintain_center_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_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: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.8),
|
||||
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: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.9),
|
||||
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
siro_driver/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart
Executable file
218
siro_driver/lib/views/home/Captin/mapDriverWidgets/driver_end_ride_bar.dart
Executable file
@@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:slide_to_act/slide_to_act.dart';
|
||||
|
||||
import '../../../../constant/colors.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
|
||||
Widget driverEndRideBar() {
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (controller) {
|
||||
// 🔥 فحص هل السعر ثابت للعرض
|
||||
final String carType = controller.carType;
|
||||
final bool isFixed = (carType == 'Speed' ||
|
||||
carType == 'Awfar' ||
|
||||
carType == 'Fixed Price');
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeOutBack,
|
||||
transform: Matrix4.translationValues(
|
||||
0,
|
||||
controller.isRideStarted ? 0 : -300,
|
||||
0,
|
||||
), // Matrix4.translationValues مستخدمة للإزاحة [web:28]
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// لوحة العدادات
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 1) المسافة (تتغير دائماً)
|
||||
_buildLiveMetric(
|
||||
icon: Icons.alt_route_rounded,
|
||||
iconColor: Colors.blueAccent,
|
||||
value: controller.currentRideDistanceKm.toStringAsFixed(2),
|
||||
unit: 'KM'.tr,
|
||||
label: 'Traveled'.tr,
|
||||
),
|
||||
|
||||
_buildVerticalDivider(),
|
||||
|
||||
// 2) السعر (ثابت أو متغير)
|
||||
_buildLiveMetric(
|
||||
icon: isFixed
|
||||
? Icons.lock_outline_rounded
|
||||
: Icons.attach_money_rounded,
|
||||
iconColor: isFixed ? Colors.grey : AppColor.primaryColor,
|
||||
value: NumberFormat('#,##0').format(
|
||||
double.tryParse(controller.price.toString()) ?? 0,
|
||||
),
|
||||
unit: 'SYP'.tr,
|
||||
label: isFixed ? 'Fixed Price'.tr : 'Meter Fare'.tr,
|
||||
isHighlight: true,
|
||||
isFixedStyle: isFixed,
|
||||
),
|
||||
|
||||
_buildVerticalDivider(),
|
||||
|
||||
// 3) الوقت (تصغير الخط + إخفاء الساعات إذا 0)
|
||||
_buildLiveMetric(
|
||||
icon: Icons.timer_outlined,
|
||||
iconColor: Colors.orange,
|
||||
value: _formatDurationFromStart(controller),
|
||||
unit: '',
|
||||
label: 'Duration'.tr,
|
||||
valueFontSize: 14, // ✅ تصغير خط “المدة”
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// زر الإنهاء
|
||||
SlideAction(
|
||||
key: ValueKey(controller.isRideFinished),
|
||||
height: 60,
|
||||
borderRadius: 18,
|
||||
elevation: 0,
|
||||
text: 'Swipe to End Trip'.tr,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
outerColor: AppColor.redColor,
|
||||
innerColor: Colors.white,
|
||||
sliderButtonIcon: const Icon(
|
||||
Icons.stop_circle_outlined,
|
||||
color: AppColor.redColor,
|
||||
size: 32,
|
||||
),
|
||||
onSubmit: () async {
|
||||
await controller.finishRideFromDriver(isFromSlider: true);
|
||||
return null;
|
||||
},
|
||||
), // SlideAction مثال الاستخدام موجود في صفحة الحزمة [web:19]
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
); // GetBuilder يعيد البناء عند update() من الكنترولر [web:21]
|
||||
}
|
||||
|
||||
/// دالة تنسيق المدة:
|
||||
/// - أقل من ساعة: mm:ss
|
||||
/// - ساعة فأكثر: h:mm:ss (خانة واحدة للساعات بدون leading zero)
|
||||
String _formatDurationFromStart(MapDriverController controller) {
|
||||
if (controller.rideStartTime == null) return "00:00";
|
||||
|
||||
final d = DateTime.now().difference(controller.rideStartTime!);
|
||||
|
||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||
|
||||
final hours = d.inHours;
|
||||
final minutes = d.inMinutes.remainder(60);
|
||||
final seconds = d.inSeconds.remainder(60);
|
||||
|
||||
if (hours == 0) {
|
||||
// mm:ss
|
||||
final totalMinutes = d.inMinutes;
|
||||
return "${twoDigits(totalMinutes)}:${twoDigits(seconds)}";
|
||||
}
|
||||
|
||||
// h:mm:ss
|
||||
return "$hours:${twoDigits(minutes)}:${twoDigits(seconds)}";
|
||||
} // Duration وتفكيكه (inHours/inMinutes/inSeconds) من أساسيات Dart [web:11]
|
||||
|
||||
Widget _buildLiveMetric({
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required String value,
|
||||
required String unit,
|
||||
required String label,
|
||||
bool isHighlight = false,
|
||||
bool isFixedStyle = false,
|
||||
double? valueFontSize, // ✅ جديد: حجم خط القيمة فقط
|
||||
}) {
|
||||
final effectiveFontSize = valueFontSize ?? (isHighlight ? 20 : 18);
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: iconColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: effectiveFontSize, // ✅ هنا صار التحكم
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isFixedStyle
|
||||
? Colors.grey[800]
|
||||
: (isHighlight ? AppColor.primaryColor : Colors.black87),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (unit.isNotEmpty) ...[
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
unit,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalDivider() {
|
||||
return Container(height: 35, width: 1, color: Colors.grey.shade300);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../../controller/profile/setting_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) => Listener(
|
||||
onPointerDown: (_) => controller.onUserMapInteraction(),
|
||||
child: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
onMapCreated: (mapController) {
|
||||
controller.onMapCreated(mapController);
|
||||
},
|
||||
mapType: Get.isRegistered<SettingController>()
|
||||
? (Get.find<SettingController>().isMapDarkMode
|
||||
? IntaleqMapType.normal
|
||||
: IntaleqMapType.light)
|
||||
: IntaleqMapType.light,
|
||||
zoomControlsEnabled: false,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: controller.smoothedLocation ?? locationController.myLocation,
|
||||
zoom: 17,
|
||||
bearing: controller.smoothedHeading,
|
||||
tilt: 60,
|
||||
),
|
||||
// padding: EdgeInsets.only(bottom: 50, top: Get.height * 0.7),
|
||||
// minMaxZoomPreference: const MinMaxZoomPreference(8, 18),
|
||||
myLocationEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
polylines: controller.polyLines.toSet(),
|
||||
markers: {
|
||||
// 🔥 Car icon — always visible, moves with GPS location on map.
|
||||
// MarkerId matches exactly with updateMarker() in controller.
|
||||
Marker(
|
||||
markerId: const MarkerId('MyLocation'),
|
||||
position: controller.smoothedLocation ?? controller.myLocation,
|
||||
rotation: controller.smoothedHeading,
|
||||
flat: true,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
icon: controller.carIcon,
|
||||
zIndex: 100,
|
||||
),
|
||||
if (!controller.isRideStarted &&
|
||||
controller.latLngPassengerLocation.latitude != 0)
|
||||
Marker(
|
||||
markerId: const MarkerId('start'),
|
||||
position: controller.latLngPassengerLocation,
|
||||
icon: controller.startIcon,
|
||||
),
|
||||
if (controller.latLngPassengerDestination.latitude != 0 ||
|
||||
controller.latLngPassengerDestination.longitude != 0)
|
||||
Marker(
|
||||
markerId: const MarkerId('end'),
|
||||
position: controller.latLngPassengerDestination,
|
||||
icon: controller.endIcon,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
62
siro_driver/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart
Executable file
62
siro_driver/lib/views/home/Captin/mapDriverWidgets/google_map_app.dart
Executable file
@@ -0,0 +1,62 @@
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_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: Icon(
|
||||
MaterialCommunityIcons.google_maps,
|
||||
size: 28,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
617
siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart
Executable file
617
siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart
Executable file
@@ -0,0 +1,617 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/home/captin/map_driver_controller.dart';
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import '../../../../controller/voice_call_controller.dart';
|
||||
import '../../../../controller/functions/launch.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../widgets/error_snakbar.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import '../../../../controller/firebase/notification_service.dart';
|
||||
import '../../../../controller/functions/crud.dart';
|
||||
import '../../../../constant/links.dart';
|
||||
import '../../../widgets/my_textField.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),
|
||||
// 🔥 [Fix Overflow] Flexible لمنع الـ overflow + تحويل المسافة
|
||||
// السيرفر يُرجع المسافة بالأمتار (5864.022)
|
||||
Flexible(
|
||||
child: Text(
|
||||
_formatDistanceDisplay(
|
||||
controller.distance),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
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) {
|
||||
_showCallSelectionDialog(
|
||||
context, controller);
|
||||
} 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),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
_showMessageOptions(context, controller),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
border:
|
||||
Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Icon(
|
||||
MaterialCommunityIcons
|
||||
.message_text_outline,
|
||||
color: AppColor.primaryColor,
|
||||
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: () {
|
||||
// 🔥 [Fix Start-Ride] استخدام Get.defaultDialog بدلاً من MyDialog
|
||||
// لأن MyDialog يستخدم Navigator.of(context, rootNavigator: true).pop()
|
||||
// الذي يتعارض مع Get.dialog() المستخدم في startRideFromDriver()
|
||||
// وقد يُسبب Get.back() اللاحق إغلاق صفحة الماب بدلاً من الـ loading dialog
|
||||
Get.defaultDialog(
|
||||
title: "Start Trip?".tr,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
middleText: "Ensure the passenger is in the car.".tr,
|
||||
barrierDismissible: true,
|
||||
radius: 14,
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF27AE60),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () async {
|
||||
// نُغلق الديالوج بـ Get.back() لضمان أن GetX يعرف أنه أُغلق
|
||||
Get.back();
|
||||
// ثم نُنفذ startRideFromDriver الذي يستخدم Get.dialog و Get.back بأمان
|
||||
await controller.startRideFromDriver();
|
||||
},
|
||||
child: Text('Start'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Cancel'.tr,
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.play_circle_fill_rounded),
|
||||
label: Text('Start Ride'.tr,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showCallSelectionDialog(
|
||||
BuildContext context, MapDriverController controller) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Call Options'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Choose how you want to call the passenger'.tr,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.green.withOpacity(0.1),
|
||||
child: const Icon(Icons.phone_android_rounded,
|
||||
color: Colors.green),
|
||||
),
|
||||
title: Text('Standard Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Uses cellular network'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
makePhoneCall(controller.passengerPhone.toString());
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.wifi_calling_3_rounded,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
title: Text('Free Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Voice call over internet'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
final voiceCtrl = Get.find<VoiceCallController>();
|
||||
final driverId = box.read(BoxName.driverID).toString();
|
||||
voiceCtrl.startCall(
|
||||
rideIdVal: controller.rideId,
|
||||
driverId: driverId,
|
||||
passengerId: controller.passengerId,
|
||||
remoteNameVal: controller.passengerName ?? "Passenger",
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) async {
|
||||
try {
|
||||
await CRUD().post(
|
||||
link: AppLink.sendChatMessage,
|
||||
payload: {
|
||||
'ride_id': controller.rideId.toString(),
|
||||
'sender_id': box.read(BoxName.driverID).toString(),
|
||||
'receiver_id': controller.passengerId.toString(),
|
||||
'sender_type': 'driver',
|
||||
'message_content': body,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore or log error
|
||||
}
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: controller.tokenPassenger.toString(),
|
||||
title: 'Driver Message'.tr,
|
||||
body: body,
|
||||
isTopic: false,
|
||||
tone: tone,
|
||||
driverList: [],
|
||||
category: 'message From Driver',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// تحويل المسافة من الأمتار إلى عرض مقروء
|
||||
/// السيرفر يُرجع المسافة بالأمتار (مثال: 5864.022)
|
||||
/// النتيجة: "5.9 km" أو "250 م"
|
||||
String _formatDistanceDisplay(String rawDistance) {
|
||||
final meters = double.tryParse(rawDistance) ?? 0.0;
|
||||
if (meters >= 1000) {
|
||||
return '${(meters / 1000).toStringAsFixed(1)} km';
|
||||
} else if (meters > 0) {
|
||||
return '${meters.toStringAsFixed(0)} م';
|
||||
}
|
||||
return rawDistance; // fallback للقيمة الأصلية
|
||||
}
|
||||
131
siro_driver/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart
Executable file
131
siro_driver/lib/views/home/Captin/mapDriverWidgets/sos_connect.dart
Executable file
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_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) {
|
||||
bool showSos = controller.isRideStarted;
|
||||
|
||||
if (!showSos) return const SizedBox();
|
||||
|
||||
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: [
|
||||
// === SOS Button ===
|
||||
_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: '0923456789',
|
||||
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), '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
siro_driver/lib/views/home/Captin/orderCaptin/call.dart
Executable file
25
siro_driver/lib/views/home/Captin/orderCaptin/call.dart
Executable file
@@ -0,0 +1,25 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:ride/constant/api_key.dart';
|
||||
// import 'package:ride/constant/box_name.dart';
|
||||
// import 'package:ride/main.dart';
|
||||
// import 'package:zego_uikit_prebuilt_call/zego_uikit_prebuilt_call.dart';
|
||||
|
||||
// class CallPage extends StatelessWidget {
|
||||
// const CallPage({Key? key, required this.callID}) : super(key: key);
|
||||
// final String callID;
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return ZegoUIKitPrebuiltCall(
|
||||
// appID: AK
|
||||
// .zegoCloudAppID, // Fill in the appID that you get from ZEGOCLOUD Admin Console.
|
||||
// appSign: AK
|
||||
// .zegoCloudAppSIGN, // Fill in the appSign that you get from ZEGOCLOUD Admin Console.
|
||||
// userID: box.read(BoxName.passengerID) ?? box.read(BoxName.driverID),
|
||||
// userName: box.read(BoxName.name) ?? box.read(BoxName.nameDriver),
|
||||
// callID: callID,
|
||||
// // You can also use groupVideo/groupVoice/oneOnOneVoice to make more types of calls.
|
||||
// config: ZegoUIKitPrebuiltCallConfig.oneOnOneVoiceCall(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class MarkerGenerator {
|
||||
// دالة لرسم ماركر يحتوي على نص (للوقت والمسافة)
|
||||
static Future<InlqBitmap> 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, 65),
|
||||
const Radius.circular(20), // زوايا أكثر استدارة لشكل عصري
|
||||
);
|
||||
|
||||
// ظل أقوى لمظهر بارز (Premium Feel)
|
||||
canvas.drawShadow(Path()..addRRect(rRect), Colors.black54, 10.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 InlqBitmap.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
// دالة خاصة لرسم ماركر السائق (دائرة وخلفها سهم)
|
||||
static Future<InlqBitmap> 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 InlqBitmap.fromBytes(data!.buffer.asUint8List());
|
||||
}
|
||||
}
|
||||
814
siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart
Executable file
814
siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart
Executable file
@@ -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:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_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)},${_getData(1)}',
|
||||
'passengerDestination': '${_getData(3)},${_getData(4)}',
|
||||
'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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
438
siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart
Executable file
438
siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart
Executable file
@@ -0,0 +1,438 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_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: IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(
|
||||
controller.latPassenger, controller.lngPassenger),
|
||||
zoom: 13.0,
|
||||
),
|
||||
markers: controller.markers,
|
||||
mapType: Get.isDarkMode
|
||||
? IntaleqMapType.normal
|
||||
: IntaleqMapType.light,
|
||||
polylines: controller.polylines,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
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: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(25),
|
||||
topRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
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: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(2)))),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// الصف الأول: الراكب والسعر
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant,
|
||||
child: Icon(Icons.person,
|
||||
color: Theme.of(context).hintColor,
|
||||
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: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
context, carIcon, carTypeLabel, carTypeColor),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
Icons.route,
|
||||
controller.totalTripDistance,
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color ??
|
||||
Colors.black87),
|
||||
Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
Icons.access_time_filled,
|
||||
controller.totalTripDuration,
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.color ??
|
||||
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(
|
||||
BuildContext context, 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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
558
siro_driver/lib/views/home/Captin/orderCaptin/order_speed_request.dart
Executable file
558
siro_driver/lib/views/home/Captin/orderCaptin/order_speed_request.dart
Executable file
@@ -0,0 +1,558 @@
|
||||
// import 'dart:convert';
|
||||
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
// import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
// import 'package:siro_driver/constant/box_name.dart';
|
||||
// import 'package:siro_driver/controller/firebase/firbase_messge.dart';
|
||||
// import 'package:siro_driver/main.dart';
|
||||
// import 'package:siro_driver/print.dart';
|
||||
// import 'package:siro_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();
|
||||
// }
|
||||
// // }
|
||||
228
siro_driver/lib/views/home/Captin/orderCaptin/test_order_page.dart
Executable file
228
siro_driver/lib/views/home/Captin/orderCaptin/test_order_page.dart
Executable file
@@ -0,0 +1,228 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.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(() => IntaleqMap(
|
||||
apiKey: AK.mapAPIKEY,
|
||||
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: (IntaleqMapController 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<IntaleqMapController?> mapController = Rx<IntaleqMapController?>(null);
|
||||
|
||||
// Icons for start and end markers
|
||||
late InlqBitmap startIcon;
|
||||
late InlqBitmap 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 = InlqBitmap.fromAsset('assets/start_marker.png');
|
||||
endIcon = InlqBitmap.fromAsset('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
|
||||
}
|
||||
}
|
||||
237
siro_driver/lib/views/home/Captin/orderCaptin/vip_order_page.dart
Executable file
237
siro_driver/lib/views/home/Captin/orderCaptin/vip_order_page.dart
Executable file
@@ -0,0 +1,237 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_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
siro_driver/lib/views/home/Captin/passportimage.dart
Executable file
46
siro_driver/lib/views/home/Captin/passportimage.dart
Executable file
@@ -0,0 +1,46 @@
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import '../../../controller/functions/ocr_controller.dart';
|
||||
//
|
||||
// class PassportDataExtractorWidget extends StatelessWidget {
|
||||
// final PassportDataExtractor passportDataExtractor =
|
||||
// Get.put(PassportDataExtractor());
|
||||
// final PassportDataController controller = Get.put(PassportDataController());
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Passport Data Extractor'),
|
||||
// ),
|
||||
// body: Column(
|
||||
// children: [
|
||||
// ElevatedButton(
|
||||
// onPressed: controller.extractDataAndDrawBoundingBoxes,
|
||||
// child: const Text('Extract Data'),
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: Center(
|
||||
// child: Stack(
|
||||
// children: [
|
||||
// GetBuilder<PassportDataController>(
|
||||
// builder: (controller) => CustomPaint(
|
||||
// painter: BoundingBoxPainter(
|
||||
// controller.extractedTextWithCoordinates),
|
||||
// child: GetBuilder<PassportDataExtractor>(
|
||||
// builder: (controller) =>
|
||||
// Image.file(File(passportDataExtractor.image!.path)),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
175
siro_driver/lib/views/home/Captin/text_scanner.dart
Executable file
175
siro_driver/lib/views/home/Captin/text_scanner.dart
Executable file
@@ -0,0 +1,175 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_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')));
|
||||
// }
|
||||
// }
|
||||
190
siro_driver/lib/views/home/journal/schedule_page.dart
Normal file
190
siro_driver/lib/views/home/journal/schedule_page.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../controller/home/journal/schedule_controller.dart';
|
||||
|
||||
class SchedulePage extends StatelessWidget {
|
||||
SchedulePage({super.key});
|
||||
final ScheduleController controller = Get.put(ScheduleController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('My Schedule'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back()),
|
||||
),
|
||||
body: GetBuilder<ScheduleController>(builder: (sc) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// Summary Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Weekly Plan'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Text('${sc.totalWeeklyHours.toStringAsFixed(1)}h',
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white)),
|
||||
Text('${sc.activeDays} ${'Days'.tr}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
fontSize: 13)),
|
||||
])),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
child: const Icon(Icons.calendar_today_rounded,
|
||||
color: Colors.white, size: 28),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Text('Work Days'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
...sc.schedule.map((slot) => _buildDayCard(context, slot, sc)),
|
||||
]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDayCard(
|
||||
BuildContext context, WorkSlot slot, ScheduleController sc) {
|
||||
final isAr = Get.locale?.languageCode == 'ar';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: slot.isActive ? Colors.white : Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: slot.isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3))
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(children: [
|
||||
// Toggle
|
||||
GestureDetector(
|
||||
onTap: () => sc.toggleDay(slot.dayOfWeek),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: slot.isActive
|
||||
? FinanceDesignSystem.accentBlue.withValues(alpha: 0.1)
|
||||
: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
isAr ? slot.dayNameAr.substring(0, 2) : slot.dayName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: slot.isActive
|
||||
? FinanceDesignSystem.accentBlue
|
||||
: Colors.grey.shade400),
|
||||
)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// Day name
|
||||
Expanded(
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(isAr ? slot.dayNameAr : slot.dayName.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: slot.isActive
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: Colors.grey.shade400)),
|
||||
if (slot.isActive)
|
||||
Text(slot.timeRange,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
|
||||
if (!slot.isActive)
|
||||
Text('Day Off'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade400,
|
||||
fontStyle: FontStyle.italic)),
|
||||
])),
|
||||
// Time pickers
|
||||
if (slot.isActive) ...[
|
||||
_timePicker(context, slot.startTime,
|
||||
(t) => sc.updateStartTime(slot.dayOfWeek, t)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text('-', style: TextStyle(color: Colors.grey.shade400))),
|
||||
_timePicker(context, slot.endTime,
|
||||
(t) => sc.updateEndTime(slot.dayOfWeek, t)),
|
||||
],
|
||||
// Toggle switch
|
||||
Switch(
|
||||
value: slot.isActive,
|
||||
onChanged: (_) => sc.toggleDay(slot.dayOfWeek),
|
||||
activeThumbColor: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timePicker(
|
||||
BuildContext context, TimeOfDay time, Function(TimeOfDay) onChanged) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final picked =
|
||||
await showTimePicker(context: context, initialTime: time);
|
||||
if (picked != null) onChanged(picked);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(
|
||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
140
siro_driver/lib/views/home/my_wallet/bank_account_egypt.dart
Executable file
140
siro_driver/lib/views/home/my_wallet/bank_account_egypt.dart
Executable file
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
313
siro_driver/lib/views/home/my_wallet/card_wallet_widget.dart
Executable file
313
siro_driver/lib/views/home/my_wallet/card_wallet_widget.dart
Executable file
@@ -0,0 +1,313 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/home/my_wallet/pay_out_screen.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.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';
|
||||
|
||||
// تذكير: ستحتاج إلى إضافة حزمة flutter_svg إلى ملف pubspec.yaml
|
||||
// dependencies:
|
||||
// flutter_svg: ^2.0.7
|
||||
|
||||
/// بطاقة المحفظة بتصميم سوري فاخر مستوحى من فن الأرابيسك والفسيفساء
|
||||
class CardSeferWalletDriver extends StatelessWidget {
|
||||
const CardSeferWalletDriver({super.key});
|
||||
|
||||
// SVG لنقشة أرابيسك هندسية لاستخدامها كخلفية
|
||||
final String arabesquePattern = '''
|
||||
<svg width="100%" height="100%" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="arabesque" patternUnits="userSpaceOnUse" width="50" height="50" patternTransform="scale(1.2)">
|
||||
<g fill="#E7C582" fill-opacity="0.1">
|
||||
<path d="M25 0 L35.35 9.65 L50 25 L35.35 40.35 L25 50 L14.65 40.35 L0 25 L14.65 9.65 Z"/>
|
||||
<path d="M50 0 L60.35 9.65 L75 25 L60.35 40.35 L50 50 L39.65 40.35 L25 25 L39.65 9.65 Z"/>
|
||||
<path d="M0 50 L9.65 39.65 L25 25 L9.65 10.35 L0 0 L-9.65 10.35 L-25 25 L-9.65 39.65 Z"/>
|
||||
</g>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#arabesque)"/>
|
||||
</svg>
|
||||
''';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: GetBuilder<CaptainWalletController>(
|
||||
builder: (captainWalletController) {
|
||||
return GestureDetector(
|
||||
onTap: () => _showCashOutDialog(context, captainWalletController),
|
||||
child: Container(
|
||||
width: Get.width * 0.9,
|
||||
height: Get.height * 0.25,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF003C43).withOpacity(0.5),
|
||||
blurRadius: 25,
|
||||
spreadRadius: -5,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Stack(
|
||||
children: [
|
||||
// الخلفية الرئيسية
|
||||
Container(color: const Color(0xFF003C43)),
|
||||
// طبقة النقشة
|
||||
SvgPicture.string(arabesquePattern, fit: BoxFit.cover),
|
||||
// طبقة التأثير الزجاجي (Glassmorphism)
|
||||
BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),
|
||||
child: Container(color: Colors.black.withOpacity(0.1)),
|
||||
),
|
||||
// محتوى البطاقة
|
||||
_buildCardContent(captainWalletController),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardContent(CaptainWalletController captainWalletController) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 20, 24, 20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'رصيد انطلق',
|
||||
style: AppStyle.headTitle.copyWith(
|
||||
fontFamily: 'Amiri', // خط يوحي بالفخامة
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
// أيقونة شريحة البطاقة
|
||||
const Icon(Icons.sim_card_outlined,
|
||||
color: Color(0xFFE7C582), size: 30),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'الرصيد الحالي'.tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// استخدام AnimatedSwitcher لإضافة حركة عند تحديث الرصيد
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'${captainWalletController.totalAmountVisa} ${'ل.س'.tr}',
|
||||
key:
|
||||
ValueKey<String>(captainWalletController.totalAmountVisa),
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: const Color(0xFFE7C582), // Antique Gold
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
box.read(BoxName.nameDriver).toString().split(' ')[0],
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"سحب الرصيد".tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCashOutDialog(
|
||||
BuildContext context, CaptainWalletController captainWalletController) {
|
||||
double minAmount = 20000.0; // الحد الأدنى للسحب
|
||||
if (double.parse(captainWalletController.totalAmountVisa) >= minAmount) {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'هل تريد سحب أرباحك؟'.tr,
|
||||
titleStyle: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.account_balance_wallet,
|
||||
color: AppColor.primaryColor, size: 30),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
'${'رصيدك الإجمالي:'.tr} ${captainWalletController.totalAmountVisa} ${'ل.س'.tr}',
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'طريقة الدفع:'.tr,
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const MyDropDownSyria(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Form(
|
||||
key: captainWalletController.formKey,
|
||||
child: MyTextForm(
|
||||
controller: captainWalletController.phoneWallet,
|
||||
label: "أدخل رقم محفظتك".tr,
|
||||
hint: "مثال: 0912345678".tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'تأكيد'.tr,
|
||||
onPressed: () async {
|
||||
box.write(
|
||||
BoxName.phoneWallet, captainWalletController.phoneWallet);
|
||||
box.write(BoxName.walletType,
|
||||
Get.find<SyrianPayoutController>().dropdownValue.toString());
|
||||
if (captainWalletController.formKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
Get.to(() => PayoutScreen(
|
||||
amountToWithdraw:
|
||||
double.parse(captainWalletController.totalAmountVisa),
|
||||
payoutPhoneNumber:
|
||||
captainWalletController.phoneWallet.text.toString(),
|
||||
walletType: Get.find<SyrianPayoutController>()
|
||||
.dropdownValue
|
||||
.toString(),
|
||||
));
|
||||
// String amountAfterFee =
|
||||
// (double.parse(captainWalletController.totalAmountVisa) - 5)
|
||||
// .toStringAsFixed(0);
|
||||
// await Get.put(PaymobPayout()).payToWalletDriverAll(
|
||||
// amountAfterFee,
|
||||
// Get.find<SyrianPayoutController>().dropdownValue.toString(),
|
||||
// captainWalletController.phoneWallet.text.toString(),
|
||||
// );
|
||||
}
|
||||
},
|
||||
kolor: AppColor.greenColor,
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'إلغاء'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
kolor: AppColor.redColor,
|
||||
));
|
||||
} else {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text("تنبيه".tr),
|
||||
content: Text(
|
||||
'${'المبلغ في محفظتك أقل من الحد الأدنى للسحب وهو'.tr} $minAmount ${'ل.س'.tr}',
|
||||
),
|
||||
actions: <Widget>[
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: Text("موافق".tr),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// هذا الكود من الملف الأصلي وهو ضروري لعمل الحوار
|
||||
class MyDropDownSyria extends StatelessWidget {
|
||||
const MyDropDownSyria({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(SyrianPayoutController());
|
||||
return GetBuilder<SyrianPayoutController>(builder: (controller) {
|
||||
return DropdownButton<String>(
|
||||
value: controller.dropdownValue,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
elevation: 16,
|
||||
style: const TextStyle(color: Colors.deepPurple, fontSize: 16),
|
||||
underline: Container(
|
||||
height: 2,
|
||||
color: Colors.deepPurpleAccent,
|
||||
),
|
||||
onChanged: (String? newValue) {
|
||||
controller.changeValue(newValue);
|
||||
},
|
||||
items: <String>['Syriatel', 'Cash Mobile', 'Sham Cash']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value.tr),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// هذا المتحكم ضروري لعمل القائمة المنسدلة
|
||||
class SyrianPayoutController extends GetxController {
|
||||
String dropdownValue = 'Syriatel';
|
||||
|
||||
void changeValue(String? newValue) {
|
||||
if (newValue != null) {
|
||||
dropdownValue = newValue;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
170
siro_driver/lib/views/home/my_wallet/ecash.dart
Normal file
170
siro_driver/lib/views/home/my_wallet/ecash.dart
Normal file
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
196
siro_driver/lib/views/home/my_wallet/pay_out_screen.dart
Normal file
196
siro_driver/lib/views/home/my_wallet/pay_out_screen.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
siro_driver/lib/views/home/my_wallet/payment_history_driver_page.dart
Executable file
81
siro_driver/lib/views/home/my_wallet/payment_history_driver_page.dart
Executable file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class PaymentHistoryDriverPage extends StatelessWidget {
|
||||
const PaymentHistoryDriverPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(DriverWalletHistoryController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Payment History'.tr,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded, color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.archive.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.history_rounded, size: 80, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text('No transactions yet'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade400, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final tx = controller.archive[index];
|
||||
final double amount = double.tryParse(tx['amount']?.toString() ?? '0') ?? 0;
|
||||
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: TransactionPreviewItem(
|
||||
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['created_at'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date: tx['created_at']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
900
siro_driver/lib/views/home/my_wallet/points_captain.dart
Executable file
900
siro_driver/lib/views/home/my_wallet/points_captain.dart
Executable file
@@ -0,0 +1,900 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
import 'package:siro_driver/controller/payment/payment_controller.dart';
|
||||
import 'package:siro_driver/controller/payment/smsPaymnet/payment_services.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/finance_design_system.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/payment/mtn_new/mtn_payment_new_screen.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import 'ecash.dart';
|
||||
|
||||
class PointsCaptain extends StatelessWidget {
|
||||
final PaymentController paymentController = Get.put(PaymentController());
|
||||
final CaptainWalletController captainWalletController =
|
||||
Get.put(CaptainWalletController());
|
||||
|
||||
PointsCaptain({
|
||||
super.key,
|
||||
required this.kolor,
|
||||
required this.countPoint,
|
||||
required this.pricePoint,
|
||||
});
|
||||
|
||||
final Color kolor;
|
||||
final String countPoint;
|
||||
final double pricePoint;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(right: 12, bottom: 4),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
elevation: 4,
|
||||
shadowColor: kolor.withValues(alpha: 0.3),
|
||||
child: InkWell(
|
||||
onTap: () => _showPaymentOptions(context),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
width: 130,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
kolor.withValues(alpha: 0.05),
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
border:
|
||||
Border.all(color: kolor.withValues(alpha: 0.2), width: 1.5),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: kolor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.account_balance_wallet_rounded,
|
||||
color: kolor, size: 24),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'$countPoint ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${'Price:'.tr} ${pricePoint.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaymentOptions(BuildContext context) {
|
||||
Get.bottomSheet(
|
||||
isScrollControlled: true,
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(32)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Select Payment Method".tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close_rounded, color: Colors.grey),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text("${'Amount to charge:'.tr} $countPoint ${'SYP'.tr}",
|
||||
style: FinanceDesignSystem.subHeadingStyle),
|
||||
const SizedBox(height: 24),
|
||||
_buildPaymentMethodTile(
|
||||
icon: Icons.credit_card_rounded,
|
||||
title: 'Debit Card'.tr,
|
||||
subtitle: 'E-Cash payment gateway'.tr,
|
||||
color: Colors.blue,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
payWithEcashDriver(context, pricePoint.toString());
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPaymentMethodTile(
|
||||
image: 'assets/images/syriatel.jpeg',
|
||||
title: 'Syriatel Cash'.tr,
|
||||
subtitle: 'Pay using Syriatel mobile wallet'.tr,
|
||||
color: Colors.red,
|
||||
onTap: () => _showPhoneInputDialog(context, 'Syriatel'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPaymentMethodTile(
|
||||
image: 'assets/images/shamCash.png',
|
||||
title: 'Sham Cash'.tr,
|
||||
subtitle: 'Pay using Sham Cash wallet'.tr,
|
||||
color: Colors.orange,
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
bool isAuthSupported =
|
||||
await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate =
|
||||
await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Confirm payment with biometrics'.tr,
|
||||
);
|
||||
if (!didAuthenticate) return;
|
||||
}
|
||||
Get.to(() => PaymentScreenSmsProvider(amount: pricePoint));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentMethodTile({
|
||||
IconData? icon,
|
||||
String? image,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.shade200, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: image != null
|
||||
? Image.asset(image, fit: BoxFit.contain)
|
||||
: Icon(icon, color: color, size: 30),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 2),
|
||||
Text(subtitle,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 14, color: Colors.grey.shade400),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPhoneInputDialog(BuildContext context, String provider) {
|
||||
Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'Wallet Phone Number'.tr,
|
||||
content: Form(
|
||||
key: paymentController.formKey,
|
||||
child: MyTextForm(
|
||||
controller: paymentController.walletphoneController,
|
||||
label: 'Phone Number'.tr,
|
||||
hint: provider == 'Syriatel' ? '963991234567' : '963941234567',
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Confirm'.tr,
|
||||
onPressed: () async {
|
||||
if (paymentController.formKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
box.write(BoxName.phoneWallet,
|
||||
paymentController.walletphoneController.text);
|
||||
if (provider == 'Syriatel') {
|
||||
await payWithSyriaTelWallet(
|
||||
context, pricePoint.toString(), 'SYP');
|
||||
} else {
|
||||
await payWithMTNWallet(context, pricePoint.toString(), 'SYP');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentScreen extends StatefulWidget {
|
||||
final String iframeUrl;
|
||||
final String countPrice;
|
||||
|
||||
const PaymentScreen(
|
||||
{required this.iframeUrl, super.key, required this.countPrice});
|
||||
|
||||
@override
|
||||
State<PaymentScreen> createState() => _PaymentScreenState();
|
||||
}
|
||||
|
||||
class _PaymentScreenState extends State<PaymentScreen> {
|
||||
late final WebViewController _controller;
|
||||
final controller = Get.find<CaptainWalletController>();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onPageFinished: (url) {
|
||||
if (url.contains("success")) {
|
||||
_fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع
|
||||
} else if (url.contains("failed")) {
|
||||
showCustomDialog(
|
||||
title: "Error".tr,
|
||||
message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
..loadRequest(Uri.parse(widget.iframeUrl));
|
||||
}
|
||||
|
||||
Future<void> _fetchPaymentStatus() async {
|
||||
final String userId = box.read(BoxName.phoneDriver);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
try {
|
||||
final response = await CRUD().postWallet(
|
||||
link: AppLink.paymetVerifyDriver,
|
||||
payload: {
|
||||
'user_id': userId,
|
||||
'driverID': box.read(BoxName.driverID),
|
||||
'paymentMethod': 'visa-in',
|
||||
},
|
||||
);
|
||||
|
||||
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.getCaptainWalletFromBuyPoints();
|
||||
|
||||
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,
|
||||
);
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "transaction_failed".tr,
|
||||
message: response['message'].toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "connection_failed".tr,
|
||||
message: response.toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showCustomDialog(
|
||||
title: "server_error".tr,
|
||||
message: "server_error_message".tr,
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showCustomDialog({
|
||||
required String title,
|
||||
required String message,
|
||||
required bool isSuccess,
|
||||
}) {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: isSuccess ? Colors.green : Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"OK",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('إتمام الدفع')),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentScreenWallet extends StatefulWidget {
|
||||
final String iframeUrl;
|
||||
final String countPrice;
|
||||
|
||||
const PaymentScreenWallet(
|
||||
{required this.iframeUrl, super.key, required this.countPrice});
|
||||
|
||||
@override
|
||||
State<PaymentScreenWallet> createState() => _PaymentScreenWalletState();
|
||||
}
|
||||
|
||||
class _PaymentScreenWalletState extends State<PaymentScreenWallet> {
|
||||
late final WebViewController _controller;
|
||||
final controller = Get.find<CaptainWalletController>();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onPageFinished: (url) {
|
||||
if (url.contains("success")) {
|
||||
_fetchPaymentStatus(); // ✅ استدعاء الويب هوك بعد نجاح الدفع
|
||||
} else if (url.contains("failed")) {
|
||||
showCustomDialog(
|
||||
title: "Error".tr,
|
||||
message: 'Payment Failed'.tr, // يتم جلب رسالة الخطأ من الخادم
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
..loadRequest(Uri.parse(widget.iframeUrl));
|
||||
}
|
||||
|
||||
Future<void> _fetchPaymentStatus() async {
|
||||
final String userId = '+963${box.read(BoxName.phoneWallet)}';
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
try {
|
||||
final response = await CRUD().postWallet(
|
||||
link: AppLink.paymetVerifyDriver,
|
||||
payload: {
|
||||
'user_id': userId,
|
||||
'driverID': box.read(BoxName.driverID),
|
||||
'paymentMethod': 'visa-in',
|
||||
},
|
||||
);
|
||||
|
||||
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.getCaptainWalletFromBuyPoints();
|
||||
|
||||
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,
|
||||
);
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "transaction_failed".tr,
|
||||
message: response['message'].toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showCustomDialog(
|
||||
title: "connection_failed".tr,
|
||||
message: response.toString(),
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showCustomDialog(
|
||||
title: "server_error".tr,
|
||||
message: "server_error_message".tr,
|
||||
isSuccess: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showCustomDialog({
|
||||
required String title,
|
||||
required String message,
|
||||
required bool isSuccess,
|
||||
}) {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: isSuccess ? Colors.green : Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"OK",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('إتمام الدفع')),
|
||||
body: WebViewWidget(controller: _controller),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payWithMTNWallet(
|
||||
BuildContext context, String amount, String currency) async {
|
||||
// استخدام مؤشر تحميل لتجربة مستخدم أفضل
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
try {
|
||||
String phone = box.read(BoxName.phoneWallet);
|
||||
String driverID = box.read(BoxName.driverID).toString();
|
||||
String formattedAmount = double.parse(amount).toStringAsFixed(0);
|
||||
|
||||
print("🚀 بدء عملية دفع MTN");
|
||||
print(
|
||||
"📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone");
|
||||
|
||||
// التحقق من البصمة (اختياري)
|
||||
bool isAuthSupported = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
if (!didAuthenticate) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
print("❌ المستخدم لم يؤكد بالبصمة/الوجه");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1️⃣ استدعاء mtn_start_payment.php (الملف الجديد)
|
||||
var responseData = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithMTNStart,
|
||||
payload: {
|
||||
"amount": formattedAmount,
|
||||
"passengerId": driverID,
|
||||
"phone": phone,
|
||||
"lang": box.read(BoxName.lang) ?? 'ar',
|
||||
},
|
||||
);
|
||||
|
||||
print("✅ استجابة الخادم (mtn_start_payment.php):");
|
||||
print(responseData);
|
||||
|
||||
// --- بداية التعديل المهم ---
|
||||
// التحقق القوي من الاستجابة لتجنب الأخطاء
|
||||
Map<String, dynamic> startRes;
|
||||
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
// إذا كانت الاستجابة بالفعل Map، استخدمها مباشرة
|
||||
startRes = responseData;
|
||||
} else if (responseData is String) {
|
||||
// إذا كانت نص، حاول تحليلها كـ JSON
|
||||
try {
|
||||
startRes = json.decode(responseData);
|
||||
} catch (e) {
|
||||
throw Exception(
|
||||
"فشل في تحليل استجابة الخادم. الاستجابة: $responseData");
|
||||
}
|
||||
} else {
|
||||
// نوع غير متوقع
|
||||
throw Exception("تم استلام نوع بيانات غير متوقع من الخادم.");
|
||||
}
|
||||
|
||||
if (startRes['status'] != 'success') {
|
||||
final errorMsg = startRes['message']['Error']?.toString().tr ??
|
||||
"فشل بدء عملية الدفع. حاول مرة أخرى.";
|
||||
throw Exception(errorMsg);
|
||||
}
|
||||
// --- نهاية التعديل المهم ---
|
||||
|
||||
// استخراج البيانات بأمان
|
||||
final messageData = startRes["message"];
|
||||
final invoiceNumber = messageData["invoiceNumber"].toString();
|
||||
final operationNumber = messageData["operationNumber"].toString();
|
||||
final guid = messageData["guid"].toString();
|
||||
|
||||
print(
|
||||
"📄 invoiceNumber: $invoiceNumber, 🔢 operationNumber: $operationNumber, 🧭 guid: $guid");
|
||||
|
||||
if (Get.isDialogOpen == true)
|
||||
Get.back(); // إغلاق مؤشر التحميل قبل عرض حوار OTP
|
||||
|
||||
// 2️⃣ عرض واجهة إدخال OTP
|
||||
String? otp = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
String input = "";
|
||||
return AlertDialog(
|
||||
title: const Text("أدخل كود التحقق"),
|
||||
content: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(hintText: "كود OTP"),
|
||||
onChanged: (val) => input = val,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("تأكيد"),
|
||||
onPressed: () => Navigator.of(context).pop(input),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("إلغاء"),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (otp == null || otp.isEmpty) {
|
||||
print("❌ لم يتم إدخال OTP");
|
||||
return;
|
||||
}
|
||||
print("🔐 تم إدخال OTP: $otp");
|
||||
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
// 3️⃣ استدعاء mtn_confirm.php
|
||||
var confirmRes = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithMTNConfirm,
|
||||
payload: {
|
||||
"invoiceNumber": invoiceNumber,
|
||||
"operationNumber": operationNumber,
|
||||
"guid": guid,
|
||||
"otp": otp,
|
||||
"phone": phone,
|
||||
},
|
||||
);
|
||||
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
print("✅ استجابة mtn_confirm.php:");
|
||||
// print(confirmRes);
|
||||
Log.print('confirmRes: ${confirmRes}');
|
||||
|
||||
if (confirmRes != null && confirmRes['status'] == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: "✅ نجاح",
|
||||
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
|
||||
);
|
||||
} else {
|
||||
String errorMsg =
|
||||
confirmRes?['message']['message']?.toString() ?? "فشل في تأكيد الدفع";
|
||||
Get.defaultDialog(
|
||||
title: "❌ فشل",
|
||||
content: Text(errorMsg.tr),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
print("🔥 خطأ أثناء الدفع عبر MTN:");
|
||||
print(e);
|
||||
print(s);
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'حدث خطأ',
|
||||
content: Text(e.toString().replaceFirst("Exception: ", "")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> payWithSyriaTelWallet(
|
||||
BuildContext context, String amount, String currency) async {
|
||||
// Show a loading indicator for better user experience
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
try {
|
||||
String phone = box.read(BoxName.phoneWallet);
|
||||
String driverID = box.read(BoxName.driverID).toString();
|
||||
String formattedAmount = double.parse(amount).toStringAsFixed(0);
|
||||
|
||||
// --- CHANGE 1: Updated log messages for clarity ---
|
||||
print("🚀 Starting Syriatel payment process");
|
||||
print(
|
||||
"📦 Payload: driverID: $driverID, amount: $formattedAmount, phone: $phone");
|
||||
|
||||
// Optional: Biometric authentication
|
||||
bool isAuthSupported = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAuthSupported) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||
);
|
||||
if (!didAuthenticate) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
print("❌ User did not authenticate with biometrics");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- CHANGE 2: Updated API link and payload for starting payment ---
|
||||
// Make sure you have defined `payWithSyriatelStart` in your AppLink class
|
||||
var responseData = await CRUD().postWalletMtn(
|
||||
link: AppLink.payWithSyriatelStart, // Use the new Syriatel start link
|
||||
payload: {
|
||||
"amount": formattedAmount,
|
||||
"driverId": driverID, // Key changed from 'passengerId' to 'driverId'
|
||||
"phone": phone,
|
||||
"lang": box.read(BoxName.lang) ?? 'ar',
|
||||
},
|
||||
);
|
||||
|
||||
print("✅ Server response (start_payment.php):");
|
||||
Log.print('responseData: ${responseData}');
|
||||
|
||||
// Robustly parse the server's JSON response
|
||||
Map<String, dynamic> startRes;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
startRes = responseData;
|
||||
} else if (responseData is String) {
|
||||
try {
|
||||
startRes = json.decode(responseData);
|
||||
} catch (e) {
|
||||
throw Exception(
|
||||
"Failed to parse server response. Response: $responseData");
|
||||
}
|
||||
} else {
|
||||
throw Exception("Received an unexpected data type from the server.");
|
||||
}
|
||||
|
||||
if (startRes['status'] != 'success') {
|
||||
String errorMsg = startRes['message']?.toString() ??
|
||||
"Failed to start the payment process. Please try again.";
|
||||
throw Exception(errorMsg);
|
||||
}
|
||||
|
||||
// --- CHANGE 3: Extract `transactionID` from the response ---
|
||||
// The response structure is now simpler. We only need the transaction ID.
|
||||
final messageData = startRes["message"];
|
||||
final transactionID = messageData["transactionID"].toString();
|
||||
|
||||
print("📄 TransactionID: $transactionID");
|
||||
|
||||
if (Get.isDialogOpen == true) Get.back(); // Close loading indicator
|
||||
|
||||
// Show the OTP input dialog
|
||||
String? otp = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
String input = "";
|
||||
return AlertDialog(
|
||||
title: const Text("أدخل كود التحقق"),
|
||||
content: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(hintText: "كود OTP"),
|
||||
onChanged: (val) => input = val,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("تأكيد"),
|
||||
onPressed: () => Navigator.of(context).pop(input),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("إلغاء"),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (otp == null || otp.isEmpty) {
|
||||
print("❌ OTP was not entered.");
|
||||
return;
|
||||
}
|
||||
print("🔐 OTP entered: $otp");
|
||||
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
// --- CHANGE 4: Updated API link and payload for confirming payment ---
|
||||
// Make sure you have defined `payWithSyriatelConfirm` in your AppLink class
|
||||
var confirmRes = await CRUD().postWalletMtn(
|
||||
// Changed from postWalletMtn if they are different
|
||||
link: AppLink.payWithSyriatelConfirm, // Use the new Syriatel confirm link
|
||||
payload: {
|
||||
"transactionID": transactionID, // Use the transaction ID
|
||||
"otp": otp,
|
||||
// The other parameters (phone, guid, etc.) are no longer needed
|
||||
},
|
||||
);
|
||||
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
print("✅ Response from confirm_payment.php:");
|
||||
Log.print('confirmRes: ${confirmRes}');
|
||||
|
||||
if (confirmRes != null && confirmRes['status'] == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: "✅ نجاح",
|
||||
content: const Text("تمت عملية الدفع وإضافة الرصيد إلى محفظتك."),
|
||||
);
|
||||
} else {
|
||||
// --- CHANGE 5: Simplified error message extraction ---
|
||||
// The new PHP script sends the error directly in the 'message' field.
|
||||
String errorMsg =
|
||||
confirmRes?['message']?.toString() ?? "فشل في تأكيد الدفع";
|
||||
Get.defaultDialog(
|
||||
title: "❌ فشل",
|
||||
content: Text(errorMsg.tr),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
// --- CHANGE 6: Updated general error log message ---
|
||||
print("🔥 Error during Syriatel Wallet payment:");
|
||||
print(e);
|
||||
print(s);
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
Get.defaultDialog(
|
||||
title: 'حدث خطأ',
|
||||
content: Text(e.toString().replaceFirst("Exception: ", "")),
|
||||
);
|
||||
}
|
||||
}
|
||||
145
siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart
Executable file
145
siro_driver/lib/views/home/my_wallet/transfer_budget_page.dart
Executable file
@@ -0,0 +1,145 @@
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_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);
|
||||
}
|
||||
}
|
||||
443
siro_driver/lib/views/home/my_wallet/walet_captain.dart
Executable file
443
siro_driver/lib/views/home/my_wallet/walet_captain.dart
Executable file
@@ -0,0 +1,443 @@
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/home/my_wallet/payment_history_driver_page.dart';
|
||||
import 'package:siro_driver/controller/payment/driver_payment_controller.dart';
|
||||
|
||||
// Import new widgets
|
||||
import 'points_captain.dart';
|
||||
import 'transfer_budget_page.dart';
|
||||
import 'widgets/balance_card.dart';
|
||||
import 'widgets/quick_actions.dart';
|
||||
import 'widgets/financial_summary_card.dart';
|
||||
import 'widgets/promo_gamification_card.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class WalletCaptainRefactored extends StatelessWidget {
|
||||
WalletCaptainRefactored({super.key});
|
||||
|
||||
final CaptainWalletController controller = Get.put(CaptainWalletController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.refreshCaptainWallet();
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Driver Balance'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh_rounded,
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
onPressed: () => controller.refreshCaptainWallet(),
|
||||
tooltip: 'Refresh'.tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GetBuilder<CaptainWalletController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FinanceDesignSystem.horizontalPadding,
|
||||
vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1. Header / Balance
|
||||
BalanceCard(
|
||||
balance: controller.totalPoints.toString(),
|
||||
isNegative:
|
||||
double.tryParse(controller.totalPoints.toString()) !=
|
||||
null &&
|
||||
double.parse(controller.totalPoints.toString()) <
|
||||
-30000,
|
||||
lastUpdated: "Just now".tr,
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 2. Quick Actions
|
||||
QuickActionsGrid(
|
||||
onAddBalance: () =>
|
||||
_showAddBalanceOptions(context, controller),
|
||||
onWithdraw: () => addSyrianPaymentMethod(controller),
|
||||
onTransfer: () => Get.to(() => TransferBudgetPage()),
|
||||
onHistory: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getArchivePayment();
|
||||
Get.to(() => const PaymentHistoryDriverPage());
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. Earnings Summary
|
||||
FinancialSummaryCard(
|
||||
title: 'Earnings Summary'.tr,
|
||||
subtitle: 'ملخص الأرباح'.tr,
|
||||
items: [
|
||||
SummaryItem(
|
||||
icon: Icons.money_rounded,
|
||||
label: 'Cash Earnings'.tr,
|
||||
amount: controller.totalAmount,
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
),
|
||||
SummaryItem(
|
||||
icon: Icons.credit_card_rounded,
|
||||
label: 'Card Earnings'.tr,
|
||||
amount: controller.totalAmountVisa,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. Recharge Balance Packages
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Recharge Balance'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
Icon(Icons.info_outline_rounded,
|
||||
size: 18, color: Colors.grey.shade400),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 140, // Increased height for modern cards
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
PointsCaptain(
|
||||
kolor: Colors.blueGrey,
|
||||
pricePoint: 100,
|
||||
countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.brown,
|
||||
pricePoint: 200,
|
||||
countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.amber,
|
||||
pricePoint: 400,
|
||||
countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.orange,
|
||||
pricePoint: 1000,
|
||||
countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 5. Promotions
|
||||
Text('Promotions'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 12),
|
||||
PromoGamificationCard(
|
||||
title: 'Morning Promo'.tr,
|
||||
subtitle: "from 7:00am to 10:00am".tr,
|
||||
currentProgress: controller.walletDate['message']?[0]
|
||||
?['morning_count'] ??
|
||||
0,
|
||||
targetProgress: 5,
|
||||
reward: "+50 SYP",
|
||||
onTap: () =>
|
||||
controller.addDriverWalletFromPromo('Morning Promo', 50),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PromoGamificationCard(
|
||||
title: 'Afternoon Promo'.tr,
|
||||
subtitle: "from 3:00pm to 6:00 pm".tr,
|
||||
currentProgress: controller.walletDate['message']?[0]
|
||||
?['afternoon_count'] ??
|
||||
0,
|
||||
targetProgress: 5,
|
||||
reward: "+50 SYP",
|
||||
onTap: () => controller.addDriverWalletFromPromo(
|
||||
'Afternoon Promo', 50),
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 6. Transactions Preview
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Recent Transactions'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Get.put(DriverWalletHistoryController())
|
||||
.getArchivePayment();
|
||||
Get.to(() => const PaymentHistoryDriverPage());
|
||||
},
|
||||
child: Text('View All'.tr,
|
||||
style: TextStyle(
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
GetBuilder<DriverWalletHistoryController>(
|
||||
init: DriverWalletHistoryController(),
|
||||
builder: (historyController) {
|
||||
if (historyController.archive.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Center(
|
||||
child: Text("No transactions yet".tr,
|
||||
style: TextStyle(color: Colors.grey.shade400))),
|
||||
);
|
||||
}
|
||||
// Show only last 3
|
||||
final lastThree =
|
||||
historyController.archive.take(3).toList();
|
||||
return Column(
|
||||
children: lastThree.map((tx) {
|
||||
final double amount =
|
||||
double.tryParse(tx['amount']?.toString() ?? '0') ??
|
||||
0;
|
||||
return TransactionPreviewItem(
|
||||
title: amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['created_at'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date: tx['created_at']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
onTap: () {},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddBalanceOptions(
|
||||
BuildContext context, CaptainWalletController controller) {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Add Balance".tr, style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 8),
|
||||
Text("Select how you want to charge your account".tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle),
|
||||
const SizedBox(height: 24),
|
||||
ListTile(
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
FinanceDesignSystem.accentBlue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(Icons.account_balance_wallet_rounded,
|
||||
color: FinanceDesignSystem.accentBlue),
|
||||
),
|
||||
title: Text("Pay from my budget".tr),
|
||||
subtitle: Text(
|
||||
"${'You have in account'.tr} ${controller.totalAmountVisa}"),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
_showPayFromBudgetDialog(controller);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
Text("Recharge Balance Packages".tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle
|
||||
.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 140,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
PointsCaptain(
|
||||
kolor: Colors.blueGrey,
|
||||
pricePoint: 100,
|
||||
countPoint: '100'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.brown, pricePoint: 200, countPoint: '210'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.amber, pricePoint: 400, countPoint: '450'),
|
||||
PointsCaptain(
|
||||
kolor: Colors.orange,
|
||||
pricePoint: 1000,
|
||||
countPoint: '1100'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPayFromBudgetDialog(CaptainWalletController controller) {
|
||||
Get.defaultDialog(
|
||||
title: 'Pay from my budget'.tr,
|
||||
content: Form(
|
||||
key: controller.formKey,
|
||||
child: MyTextForm(
|
||||
controller: controller.amountFromBudgetController,
|
||||
label: '${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
hint: '${'You have in account'.tr} ${controller.totalAmountVisa}',
|
||||
type: TextInputType.number,
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Pay'.tr,
|
||||
onPressed: () async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment'.tr,
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
if (double.parse(controller.amountFromBudgetController.text) <
|
||||
double.parse(controller.totalAmountVisa)) {
|
||||
await controller.payFromBudget();
|
||||
} else {
|
||||
Get.back();
|
||||
mySnackeBarError('Your Budget less than needed'.tr);
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Authentication failed'.tr, ''.tr, () => Get.back());
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr,
|
||||
() => Get.back());
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(title: 'Cancel'.tr, onPressed: () => Get.back()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> addSyrianPaymentMethod(
|
||||
CaptainWalletController captainWalletController) {
|
||||
return Get.defaultDialog(
|
||||
title: "Insert Payment Details".tr,
|
||||
content: Form(
|
||||
key: captainWalletController.formKeyAccount,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Insert your mobile wallet details to receive your money weekly"
|
||||
.tr),
|
||||
MyTextForm(
|
||||
controller: captainWalletController.cardBank,
|
||||
label: "Insert mobile wallet number".tr,
|
||||
hint: '0912 345 678',
|
||||
type: TextInputType.phone),
|
||||
const SizedBox(height: 10),
|
||||
MyDropDownSyria()
|
||||
],
|
||||
)),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () async {
|
||||
if (captainWalletController.formKeyAccount.currentState!
|
||||
.validate()) {
|
||||
Get.back();
|
||||
var res =
|
||||
await CRUD().post(link: AppLink.updateAccountBank, payload: {
|
||||
"paymentProvider":
|
||||
Get.find<SyrianPayoutController>().dropdownValue.toString(),
|
||||
"accountNumber":
|
||||
captainWalletController.cardBank.text.toString(),
|
||||
"id": box.read(BoxName.driverID).toString()
|
||||
});
|
||||
if (res != 'failure') {
|
||||
mySnackbarSuccess('Payment details added successfully'.tr);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
class SyrianPayoutController extends GetxController {
|
||||
String dropdownValue = 'syriatel';
|
||||
void changeValue(String? newValue) {
|
||||
if (newValue != null) {
|
||||
dropdownValue = newValue;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyDropDownSyria extends StatelessWidget {
|
||||
const MyDropDownSyria({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(SyrianPayoutController());
|
||||
final theme = Theme.of(context);
|
||||
return GetBuilder<SyrianPayoutController>(builder: (controller) {
|
||||
return DropdownButton<String>(
|
||||
value: controller.dropdownValue,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
elevation: 16,
|
||||
isExpanded: true,
|
||||
dropdownColor: theme.cardColor,
|
||||
style: TextStyle(color: theme.textTheme.bodyLarge?.color),
|
||||
underline: Container(height: 2, color: theme.primaryColor),
|
||||
onChanged: (String? newValue) => controller.changeValue(newValue),
|
||||
items: <String>['syriatel', 'mtn']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(value: value, child: Text(value.tr));
|
||||
}).toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
224
siro_driver/lib/views/home/my_wallet/weekly_payment_page.dart
Executable file
224
siro_driver/lib/views/home/my_wallet/weekly_payment_page.dart
Executable file
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
import 'widgets/transaction_preview_item.dart';
|
||||
|
||||
class WeeklyPaymentPage extends StatelessWidget {
|
||||
const WeeklyPaymentPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(DriverWalletHistoryController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Weekly Summary'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: FinanceDesignSystem.primaryDark, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildWeeklyStatsHeader(controller),
|
||||
const SizedBox(height: 16),
|
||||
_buildWeekSelector(),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('Transactions this week'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
const Spacer(),
|
||||
Icon(Icons.list_rounded,
|
||||
color: Colors.grey.shade400, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: controller.weeklyList.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
||||
itemCount: controller.weeklyList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final tx = controller.weeklyList[index];
|
||||
final double amount = double.tryParse(
|
||||
tx['amount']?.toString() ?? '0') ??
|
||||
0;
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 25,
|
||||
child: FadeInAnimation(
|
||||
child: TransactionPreviewItem(
|
||||
title:
|
||||
amount >= 0 ? 'Credit'.tr : 'Debit'.tr,
|
||||
subtitle: tx['dateUpdated'] ?? '',
|
||||
amount: amount.abs().toStringAsFixed(0),
|
||||
date:
|
||||
tx['dateUpdated']?.split(' ')[0] ?? '',
|
||||
type: amount >= 0 ? 'credit' : 'debit',
|
||||
method: tx['paymentMethod'],
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeeklyStatsHeader(DriverWalletHistoryController controller) {
|
||||
final totalAmount = controller.weeklyList.isEmpty
|
||||
? '0.00'
|
||||
: controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
|
||||
|
||||
final double earnings = double.tryParse(totalAmount) ?? 0;
|
||||
final int trips = controller.weeklyList.length;
|
||||
final double commission = earnings * 0.15; // Example 15%
|
||||
final double netProfit = earnings - commission;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: FinanceDesignSystem.primaryDark.withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('Total Weekly Earnings'.tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8), fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Text('${earnings.toStringAsFixed(0)} SYP',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildStatItem('Total Trips'.tr, trips.toString(),
|
||||
Icons.directions_car_rounded),
|
||||
_buildStatItem('Commission'.tr, commission.toStringAsFixed(0),
|
||||
Icons.percent_rounded),
|
||||
_buildStatItem('Net Profit'.tr, netProfit.toStringAsFixed(0),
|
||||
Icons.account_balance_rounded),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value, IconData icon) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon, color: Colors.white, size: 18),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14)),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.6), fontSize: 10)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeekSelector() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02), blurRadius: 10)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left_rounded), onPressed: () {}),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text('Dec 15 - Dec 21, 2024'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right_rounded),
|
||||
onPressed: () {}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.bar_chart_rounded, size: 64, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 16),
|
||||
Text('No transactions this week'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
124
siro_driver/lib/views/home/my_wallet/widgets/balance_card.dart
Normal file
124
siro_driver/lib/views/home/my_wallet/widgets/balance_card.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class BalanceCard extends StatelessWidget {
|
||||
final String balance;
|
||||
final bool isNegative;
|
||||
final String lastUpdated;
|
||||
|
||||
const BalanceCard({
|
||||
super.key,
|
||||
required this.balance,
|
||||
required this.isNegative,
|
||||
this.lastUpdated = "Just now",
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
gradient: isNegative
|
||||
? FinanceDesignSystem.dangerGradient
|
||||
: FinanceDesignSystem.balanceGradient,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (isNegative
|
||||
? FinanceDesignSystem.dangerRed
|
||||
: FinanceDesignSystem.primaryDark)
|
||||
.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Available Balance".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"الرصيد المتاح".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isNegative
|
||||
? Icons.warning_rounded
|
||||
: Icons.account_balance_wallet_rounded,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
balance,
|
||||
style: FinanceDesignSystem.balanceStyle,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"SYP".tr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Divider(color: Colors.white.withOpacity(0.15)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history_rounded,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"${'Last updated:'.tr} $lastUpdated",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class FinancialSummaryCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final List<SummaryItem> items;
|
||||
|
||||
const FinancialSummaryCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.items,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: FinanceDesignSystem.headingStyle),
|
||||
if (subtitle != null)
|
||||
Text(subtitle!, style: FinanceDesignSystem.subHeadingStyle),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
Divider(color: Colors.grey.withValues(alpha: 0.1), height: 24),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(item.icon, color: item.color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
if (item.subLabel != null)
|
||||
Text(
|
||||
item.subLabel!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${item.amount} ${'SYP'.tr}",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
if (item.trend != null)
|
||||
Text(
|
||||
item.trend!,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: item.trendColor ??
|
||||
FinanceDesignSystem.successGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SummaryItem {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String? subLabel;
|
||||
final String amount;
|
||||
final Color color;
|
||||
final String? trend;
|
||||
final Color? trendColor;
|
||||
|
||||
SummaryItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.subLabel,
|
||||
required this.amount,
|
||||
required this.color,
|
||||
this.trend,
|
||||
this.trendColor,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class PromoGamificationCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final int currentProgress;
|
||||
final int targetProgress;
|
||||
final String reward;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const PromoGamificationCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.currentProgress,
|
||||
required this.targetProgress,
|
||||
required this.reward,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double progress = (currentProgress / targetProgress).clamp(0.0, 1.0);
|
||||
final bool isCompleted = progress >= 1.0;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: (isCompleted
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue)
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
reward,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isCompleted
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${'Progress:'.tr} $currentProgress / $targetProgress",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
if (isCompleted)
|
||||
Icon(Icons.check_circle_rounded,
|
||||
color: FinanceDesignSystem.successGreen, size: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 10,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
height: 10,
|
||||
width: MediaQuery.of(context).size.width *
|
||||
progress, // Simplified for now
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isCompleted
|
||||
? [
|
||||
FinanceDesignSystem.successGreen,
|
||||
Colors.lightGreenAccent
|
||||
]
|
||||
: [FinanceDesignSystem.accentBlue, Colors.blueAccent],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isCompleted && onTap != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: FinanceDesignSystem.successGreen,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
),
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: Text("Claim Reward".tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
120
siro_driver/lib/views/home/my_wallet/widgets/quick_actions.dart
Normal file
120
siro_driver/lib/views/home/my_wallet/widgets/quick_actions.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class QuickActionsGrid extends StatelessWidget {
|
||||
final VoidCallback onAddBalance;
|
||||
final VoidCallback onWithdraw;
|
||||
final VoidCallback onTransfer;
|
||||
final VoidCallback onHistory;
|
||||
|
||||
const QuickActionsGrid({
|
||||
super.key,
|
||||
required this.onAddBalance,
|
||||
required this.onWithdraw,
|
||||
required this.onTransfer,
|
||||
required this.onHistory,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
_ActionItem(
|
||||
icon: Icons.add_circle_outline_rounded,
|
||||
label: "Add Balance".tr,
|
||||
subLabel: "شحن",
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
onTap: onAddBalance,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.file_download_outlined,
|
||||
label: "Withdraw".tr,
|
||||
subLabel: "سحب",
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
onTap: onWithdraw,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.swap_horiz_rounded,
|
||||
label: "Transfer".tr,
|
||||
subLabel: "تحويل",
|
||||
color: Colors.orange,
|
||||
onTap: onTransfer,
|
||||
),
|
||||
_ActionItem(
|
||||
icon: Icons.history_rounded,
|
||||
label: "History".tr,
|
||||
subLabel: "السجل",
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
onTap: onHistory,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String subLabel;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.subLabel,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
borderRadius:
|
||||
BorderRadius.circular(FinanceDesignSystem.buttonRadius),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
subLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class TransactionPreviewItem extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String amount;
|
||||
final String date;
|
||||
final String type; // 'credit' or 'debit'
|
||||
final String? method;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const TransactionPreviewItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.amount,
|
||||
required this.date,
|
||||
required this.type,
|
||||
this.method,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isCredit = type.toLowerCase() == 'credit';
|
||||
final Color statusColor = isCredit
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.dangerRed;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.mainRadius),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isCredit
|
||||
? Icons.arrow_downward_rounded
|
||||
: Icons.arrow_upward_rounded,
|
||||
color: statusColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
if (method != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade400,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
method!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${isCredit ? '+' : '-'}$amount ${'SYP'.tr}",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
siro_driver/lib/views/home/on_boarding_page.dart
Executable file
33
siro_driver/lib/views/home/on_boarding_page.dart
Executable file
@@ -0,0 +1,33 @@
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/auth/onboarding_controller.dart';
|
||||
import 'package:siro_driver/onbording_page.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class OnBoardingPage extends StatelessWidget {
|
||||
OnBoardingControllerImp onBoardingControllerImp =
|
||||
Get.put(OnBoardingControllerImp());
|
||||
|
||||
OnBoardingPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
body: SafeArea(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
SizedBox(
|
||||
height: Get.height * .7,
|
||||
child: const CustomSliderOnBoarding(),
|
||||
),
|
||||
const CustomDotControllerOnBoarding(),
|
||||
// const Spacer(flex: 2),
|
||||
const SizedBox(height: 20),
|
||||
MyElevatedButton(
|
||||
onPressed: () => onBoardingControllerImp.next(),
|
||||
title: 'Next'.tr,
|
||||
)
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
214
siro_driver/lib/views/home/profile/behavior_page.dart
Normal file
214
siro_driver/lib/views/home/profile/behavior_page.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/finance_design_system.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(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text('Driver Behavior'.tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
centerTitle: true,
|
||||
backgroundColor: FinanceDesignSystem.cardColor,
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: FinanceDesignSystem.primaryDark),
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.accentBlue));
|
||||
}
|
||||
|
||||
double score = controller.overallScore.value;
|
||||
bool isExcellent = score >= 90;
|
||||
bool isGood = score >= 75 && score < 90;
|
||||
Color statusColor =
|
||||
isExcellent ? Colors.green : (isGood ? Colors.orange : Colors.red);
|
||||
String statusText = isExcellent
|
||||
? 'Excellent'.tr
|
||||
: (isGood ? 'Good'.tr : 'Needs Improvement'.tr);
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding:
|
||||
const EdgeInsets.all(FinanceDesignSystem.horizontalPadding),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Overall Score Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.shield_rounded,
|
||||
size: 48, color: statusColor),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Overall Behavior Score".tr,
|
||||
style: FinanceDesignSystem.headingStyle,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"${score.toStringAsFixed(1)} / 100",
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Text("Last 10 Trips".tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
const SizedBox(height: 16),
|
||||
]),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FinanceDesignSystem.horizontalPadding),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
var trip = controller.lastTrips[index];
|
||||
double tripScore =
|
||||
double.tryParse(trip['behavior_score'].toString()) ?? 0;
|
||||
Color tColor = tripScore >= 90
|
||||
? Colors.green
|
||||
: (tripScore >= 75 ? Colors.orange : Colors.red);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: tColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.drive_eta_rounded, color: tColor),
|
||||
),
|
||||
title: Text(
|
||||
"Trip ID: ${trip['trip_id']}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildBadge(Icons.speed,
|
||||
"${trip['max_speed']} km/h", Colors.blue),
|
||||
_buildBadge(
|
||||
Icons.warning_amber_rounded,
|
||||
"${trip['hard_brakes']} ${'Hard Brakes'.tr}",
|
||||
Colors.orange),
|
||||
_buildBadge(
|
||||
Icons.map_rounded,
|
||||
"${trip['total_distance']} km",
|
||||
Colors.purple),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${tripScore.toStringAsFixed(0)}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
color: tColor),
|
||||
),
|
||||
Text('Score'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: controller.lastTrips.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 40)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBadge(IconData icon, String label, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: color, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
171
siro_driver/lib/views/home/profile/captains_cars.dart
Executable file
171
siro_driver/lib/views/home/profile/captains_cars.dart
Executable file
@@ -0,0 +1,171 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_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.symmetric(horizontal: 12.0, vertical: 6.0),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? AppColor.primaryColor
|
||||
: Theme.of(context).cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: car['isDefault'].toString() == '1'
|
||||
? BorderSide.none
|
||||
: BorderSide(color: Theme.of(context).dividerColor)),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white12,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Fontisto.car,
|
||||
size: 32,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: Color(int.parse(car['color_hex']
|
||||
.replaceFirst('#', '0xff'))),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
car['make'],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
car['model'],
|
||||
style: TextStyle(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white70
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white54
|
||||
: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
car['car_plate'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white
|
||||
: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
car['year'],
|
||||
style: TextStyle(
|
||||
color: car['isDefault'].toString() == '1'
|
||||
? Colors.white70
|
||||
: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
300
siro_driver/lib/views/home/profile/cars_inserting_page.dart
Executable file
300
siro_driver/lib/views/home/profile/cars_inserting_page.dart
Executable file
@@ -0,0 +1,300 @@
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../controller/functions/gemeni.dart';
|
||||
import '../../auth/captin/driver_car_controller.dart';
|
||||
|
||||
class CarsInsertingPage extends StatelessWidget {
|
||||
CarsInsertingPage({super.key});
|
||||
final driverCarController = Get.put(DriverCarController());
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(AI());
|
||||
return MyScafolld(
|
||||
title: "Add new car".tr,
|
||||
body: [
|
||||
Container(
|
||||
color: AppColor.accentColor.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(22),
|
||||
child: ListView(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
syriaCarLicenceFront(),
|
||||
syriaCarLicenceBack(),
|
||||
const SizedBox(height: 10),
|
||||
Text('Please make sure to read the license carefully.'.tr),
|
||||
Text(
|
||||
'If your car license has the new design, upload the front side with two images.'
|
||||
.tr),
|
||||
const SizedBox(height: 10),
|
||||
MyElevatedButton(
|
||||
title: 'Please upload this license.'.tr,
|
||||
onPressed: () {
|
||||
final aiFront =
|
||||
Get.find<AI>().responseIdCardDriverEgyptFront;
|
||||
final aiBack =
|
||||
Get.find<AI>().responseIdCardDriverEgyptBack;
|
||||
driverCarController.addCarsForDrivers(
|
||||
aiBack['chassis'].toString(),
|
||||
aiBack['car_plate'].toString(),
|
||||
aiBack['make'].toString(),
|
||||
aiBack['model'].toString(),
|
||||
aiBack['year'].toString(),
|
||||
aiFront['LicenseExpirationDate'].toString(),
|
||||
aiBack['color'].toString(),
|
||||
aiBack['color_hex'].toString(),
|
||||
aiFront['address'].toString(),
|
||||
aiFront['owner'].toString(),
|
||||
aiBack['inspection_date'].toString(),
|
||||
aiBack['engine'].toString(),
|
||||
aiBack['fuel'].toString(),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
}
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaCarLicenceFront() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
|
||||
// No need to access ai.responseIdCardDriverEgyptBack anymore
|
||||
final licenseExpiryDate = DateTime.parse(
|
||||
ai.responseIdCardDriverEgyptFront['LicenseExpirationDate']);
|
||||
|
||||
// Check if license has expired
|
||||
final today = DateTime.now();
|
||||
final isLicenseExpired = licenseExpiryDate.isBefore(today);
|
||||
|
||||
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(
|
||||
children: [
|
||||
Text('Vehicle Details Front'.tr,
|
||||
style: AppStyle.headTitle2),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
ai.allMethodForAI((ai.prompts[3]['prompt'].toString()),
|
||||
AppLink.uploadEgypt, 'car_front');
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(color: AppColor.accentColor),
|
||||
const SizedBox(height: 8.0),
|
||||
// Removed Make, Model, etc. as they are not available
|
||||
|
||||
Text(
|
||||
'${'Plate Number'.tr}: ${ai.responseIdCardDriverEgyptFront['car_plate']}',
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'Owner Name'.tr}: ${ai.responseIdCardDriverEgyptFront['owner']}',
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'Address'.tr}: ${ai.responseIdCardDriverEgyptFront['address']}',
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'License Expiry Date'.tr}: ${licenseExpiryDate.toString().substring(0, 10)}',
|
||||
style: TextStyle(
|
||||
color: isLicenseExpired ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
// Removed Fuel as it's not available
|
||||
],
|
||||
),
|
||||
// Removed Inspection Date as it's not available
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
ai.allMethodForAINewCar((ai.prompts[3]['prompt'].toString()),
|
||||
AppLink.uploadEgypt1, 'car_front', 'carId'); //todo
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/3.png',
|
||||
height: Get.height * .25,
|
||||
width: double.maxFinite,
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
Text(
|
||||
'Capture an Image of Your car license front '.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<AI> syriaCarLicenceBack() {
|
||||
return GetBuilder<AI>(
|
||||
builder: (ai) {
|
||||
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
|
||||
// Get the tax expiry date from the response
|
||||
final taxExpiryDate =
|
||||
ai.responseIdCardDriverEgyptBack['tax_expiry'].toString();
|
||||
// final displacement = ai.responseIdCardDriverEgyptBack['displacement'];
|
||||
// if (int.parse(displacement) < 1000) {}
|
||||
// Get the inspection date from the response
|
||||
final inspectionDate =
|
||||
ai.responseIdCardDriverEgyptBack['inspection_date'].toString();
|
||||
final year = int.parse(inspectionDate.toString().split('-')[0]);
|
||||
|
||||
// Set inspectionDateTime to December 31st of the given year
|
||||
final inspectionDateTime = DateTime(year, 12, 31);
|
||||
String carBackLicenseExpired =
|
||||
inspectionDateTime.toString().split(' ')[0];
|
||||
// Get the current date
|
||||
final today = DateTime.now();
|
||||
|
||||
// Try parsing the tax expiry date. If it fails, set it to null.
|
||||
final taxExpiryDateTime = DateTime.tryParse(taxExpiryDate ?? '');
|
||||
final isExpired =
|
||||
taxExpiryDateTime != null && taxExpiryDateTime.isBefore(today);
|
||||
// Check if the inspection date is before today
|
||||
bool isInspectionExpired = inspectionDateTime.isBefore(today);
|
||||
|
||||
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: () async {
|
||||
ai.allMethodForAI((ai.prompts[4]['prompt'].toString()),
|
||||
AppLink.uploadEgypt, 'car_back');
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(color: AppColor.accentColor),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Make'.tr}: ${ai.responseIdCardDriverEgyptBack['make']}'),
|
||||
Text(
|
||||
'${'Model'.tr}: ${ai.responseIdCardDriverEgyptBack['model']}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Year'.tr}: ${ai.responseIdCardDriverEgyptBack['year']}'),
|
||||
Text(
|
||||
'${'Chassis'.tr}: ${ai.responseIdCardDriverEgyptBack['chassis']}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Color'.tr}: ${ai.responseIdCardDriverEgyptBack['color']}'),
|
||||
Text(
|
||||
'${'Displacement'.tr}: ${ai.responseIdCardDriverEgyptBack['displacement']} cc'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
'${'Fuel'.tr}: ${ai.responseIdCardDriverEgyptBack['fuel']}'),
|
||||
const SizedBox(height: 8.0),
|
||||
if (taxExpiryDateTime != null)
|
||||
Text(
|
||||
'${'Tax Expiry Date'.tr}: $taxExpiryDate',
|
||||
style: TextStyle(
|
||||
color: isExpired ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Inspection Date'.tr}: $carBackLicenseExpired',
|
||||
style: TextStyle(
|
||||
color: isInspectionExpired ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
ai.allMethodForAI((ai.prompts[4]['prompt'].toString()),
|
||||
AppLink.uploadEgypt, 'car_back');
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/4.png',
|
||||
height: Get.height * .25,
|
||||
width: double.maxFinite,
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
Text(
|
||||
'Capture an Image of Your car license back'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
278
siro_driver/lib/views/home/profile/complaint_page.dart
Normal file
278
siro_driver/lib/views/home/profile/complaint_page.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/home/profile/complaint_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/functions/audio_recorder_controller.dart';
|
||||
|
||||
class ComplaintPage extends StatelessWidget {
|
||||
ComplaintPage({super.key});
|
||||
|
||||
final ComplaintController complaintController =
|
||||
Get.put(ComplaintController());
|
||||
final AudioRecorderController audioRecorderController =
|
||||
Get.put(AudioRecorderController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Submit a Complaint'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<ComplaintController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading && controller.ridesList.isEmpty) {
|
||||
return const MyCircularProgressIndicator();
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Form(
|
||||
key: controller.formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
// --- 1. Select Ride Section ---
|
||||
_buildSectionCard(
|
||||
title: '1. Select Ride'.tr,
|
||||
child: controller.ridesList.isEmpty
|
||||
? Text('No rides found to complain about.'.tr,
|
||||
style: AppStyle.subtitle)
|
||||
: DropdownButtonFormField<Map<String, dynamic>>(
|
||||
value: controller.selectedRide,
|
||||
dropdownColor: AppColor.surfaceColor,
|
||||
items: controller.ridesList.map((ride) {
|
||||
return DropdownMenuItem<Map<String, dynamic>>(
|
||||
value: ride,
|
||||
child: Text(
|
||||
'${'Ride'.tr} #${ride['id']} (${ride['date']})',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (ride) {
|
||||
if (ride != null) {
|
||||
controller.selectRide(ride);
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor:
|
||||
AppColor.secondaryColor.withOpacity(0.5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// --- 2. Describe Your Issue Section ---
|
||||
_buildSectionCard(
|
||||
title: '2. Describe Your Issue'.tr,
|
||||
child: TextFormField(
|
||||
controller: controller.complaintController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your complaint here...'.tr,
|
||||
filled: true,
|
||||
fillColor:
|
||||
AppColor.secondaryColor.withOpacity(0.5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
maxLines: 6,
|
||||
style: AppStyle.subtitle,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a description of the issue.'
|
||||
.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// --- 3. Attach Recorded Audio Section ---
|
||||
if (controller.selectedRide != null)
|
||||
_buildSectionCard(
|
||||
title: '3. Attach Recorded Audio (Optional)'.tr,
|
||||
child: FutureBuilder<List<String>>(
|
||||
future: audioRecorderController.getRecordedFiles(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.waiting) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final rideId =
|
||||
controller.selectedRide!['id'].toString();
|
||||
// Filter files to only show the audio file associated with the selected Ride ID
|
||||
final matchingFiles = snapshot.data
|
||||
?.where((path) =>
|
||||
path.endsWith('_${rideId}.m4a'))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (snapshot.hasError || matchingFiles.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0),
|
||||
child: Text(
|
||||
'No audio files found for this ride.'.tr,
|
||||
style: AppStyle.subtitle),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: matchingFiles.map((audioFilePath) {
|
||||
final audioFile = File(audioFilePath);
|
||||
final isUploaded =
|
||||
controller.audioLink.isNotEmpty &&
|
||||
controller.attachedFileName ==
|
||||
audioFilePath.split('/').last;
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isUploaded
|
||||
? Icons.check_circle
|
||||
: Icons.mic,
|
||||
color: isUploaded
|
||||
? AppColor.greenColor
|
||||
: AppColor.redColor),
|
||||
title: Text(audioFilePath.split('/').last,
|
||||
style: AppStyle.subtitle,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
subtitle: isUploaded
|
||||
? Text('Uploaded'.tr,
|
||||
style: const TextStyle(
|
||||
color: AppColor.greenColor))
|
||||
: null,
|
||||
onTap: isUploaded
|
||||
? null
|
||||
: () {
|
||||
MyDialogContent().getDialog(
|
||||
'Confirm Attachment'.tr,
|
||||
Text(
|
||||
'Attach this audio file?'
|
||||
.tr), () async {
|
||||
await controller
|
||||
.uploadAudioFile(audioFile);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// --- 4. Review Details & Response Section ---
|
||||
if (controller.selectedRide != null)
|
||||
_buildSectionCard(
|
||||
title: '4. Review Details & Response'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
Icons.calendar_today_outlined,
|
||||
'Date'.tr,
|
||||
controller.selectedRide!['date'] ?? ''),
|
||||
_buildDetailRow(
|
||||
Icons.monetization_on_outlined,
|
||||
'Price'.tr,
|
||||
'${controller.selectedRide!['price'] ?? ''}'),
|
||||
const Divider(height: 24),
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.support_agent_outlined,
|
||||
color: AppColor.primaryColor),
|
||||
title: Text("Intaleq's Response".tr,
|
||||
style: AppStyle.title),
|
||||
subtitle: Text(
|
||||
controller.driverReport?['body']
|
||||
?.toString() ??
|
||||
'Awaiting response...'.tr,
|
||||
style:
|
||||
AppStyle.subtitle.copyWith(height: 1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- 5. Submit Button ---
|
||||
const SizedBox(height: 24),
|
||||
MyElevatedButton(
|
||||
onPressed: () async {
|
||||
await controller.submitComplaintToServer();
|
||||
},
|
||||
title: 'Submit Complaint'.tr,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.isLoading)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
child: const MyCircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionCard({required String title, required Widget child}) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: AppStyle.headTitle.copyWith(fontSize: 18)),
|
||||
const SizedBox(height: 12),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(IconData icon, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: AppColor.writeColor.withOpacity(0.6), size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text('${label.tr}:',
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.writeColor.withOpacity(0.7))),
|
||||
const Spacer(),
|
||||
Text(value,
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
60
siro_driver/lib/views/home/profile/feed_back_page.dart
Executable file
60
siro_driver/lib/views/home/profile/feed_back_page.dart
Executable file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/profile/feed_back_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_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,
|
||||
);
|
||||
}
|
||||
}
|
||||
303
siro_driver/lib/views/home/profile/passenger_profile_page.dart
Executable file
303
siro_driver/lib/views/home/profile/passenger_profile_page.dart
Executable file
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/profile/profile_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
import 'package:siro_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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
586
siro_driver/lib/views/home/profile/profile_captain.dart
Executable file
586
siro_driver/lib/views/home/profile/profile_captain.dart
Executable file
@@ -0,0 +1,586 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/profile/captain_profile_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:siro_driver/views/widgets/mycircular.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import 'behavior_page.dart';
|
||||
import 'complaint_page.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) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: theme.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.person, size: 60, color: theme.primaryColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
name,
|
||||
style: theme.textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.star, color: Colors.amber, size: 20),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${rating.toStringAsFixed(1)} (${'reviews'.tr} $ratingCount)',
|
||||
style:
|
||||
theme.textTheme.titleMedium?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()),
|
||||
),
|
||||
_ActionTile(
|
||||
title: 'Submit a Complaint'.tr,
|
||||
icon: Icons.note_add_rounded,
|
||||
onTap: () => Get.to(() => ComplaintPage()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
Builder(builder: (context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(25),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.shadowColor.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const 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: theme.dividerColor,
|
||||
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) {
|
||||
final theme = Theme.of(context);
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
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: theme.primaryColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.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) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: theme.hintColor.withOpacity(0.6), size: 20),
|
||||
const SizedBox(width: 16),
|
||||
Text(label, style: theme.textTheme.bodyLarge),
|
||||
const Spacer(),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.textTheme.bodyLarge?.color?.withOpacity(0.8),
|
||||
fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
146
siro_driver/lib/views/home/profile/promos_passenger_page.dart
Executable file
146
siro_driver/lib/views/home/profile/promos_passenger_page.dart
Executable file
@@ -0,0 +1,146 @@
|
||||
import 'package:animated_text_kit/animated_text_kit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/profile/promos_controller.dart';
|
||||
import 'package:siro_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.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shadowColor:
|
||||
Theme.of(context).shadowColor.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: AnimatedTextKit(
|
||||
animatedTexts: [
|
||||
ScaleAnimatedText(
|
||||
rides['promo_code'],
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
color: AppColor
|
||||
.primaryColor,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
)),
|
||||
WavyAnimatedText(
|
||||
rides['promo_code'],
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(
|
||||
color: AppColor
|
||||
.primaryColor,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
)),
|
||||
],
|
||||
repeatForever: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
rides['description'],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_buildDateBadge(context,
|
||||
rides['validity_start_date'], true),
|
||||
const SizedBox(height: 4),
|
||||
_buildDateBadge(context,
|
||||
rides['validity_end_date'], false),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Copy this Promo to use it in your Ride!'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateBadge(BuildContext context, String date, bool isStart) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isStart
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.red.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isStart ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
95
siro_driver/lib/views/home/profile/taarif_page.dart
Executable file
95
siro_driver/lib/views/home/profile/taarif_page.dart
Executable file
@@ -0,0 +1,95 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/views/widgets/my_scafold.dart';
|
||||
|
||||
import '../../../constant/colors.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: [
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Minimum fare'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '1 ${'JOD'.tr}'
|
||||
: '20 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Maximum fare'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '200 ${'JOD'.tr}'
|
||||
: '15000 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Flag-down fee'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '0.47 ${'JOD'.tr}'
|
||||
: '15 ${'LE'.tr}'),
|
||||
_buildTariffItem(
|
||||
context,
|
||||
'Rate'.tr,
|
||||
box.read(BoxName.countryCode) == 'Jordan'
|
||||
? '0.05 ${'JOD'.tr}/min and 0.21 ${'JOD'.tr}/km'
|
||||
: '1 ${'LE'.tr}/min and 4 ${'LE'.tr}/km'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text('Including Tax'.tr,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: Theme.of(context).hintColor)),
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTariffItem(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(label, style: AppStyle.title),
|
||||
),
|
||||
Text(value,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold, color: AppColor.primaryColor)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
519
siro_driver/lib/views/home/statistics/statistics_dashboard.dart
Normal file
519
siro_driver/lib/views/home/statistics/statistics_dashboard.dart
Normal file
@@ -0,0 +1,519 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/finance_design_system.dart';
|
||||
import 'package:siro_driver/controller/gamification/gamification_controller.dart';
|
||||
import 'package:siro_driver/controller/home/statistics/statistics_controller.dart';
|
||||
|
||||
import 'widgets/stat_summary_card.dart';
|
||||
import 'widgets/daily_goal_widget.dart';
|
||||
import 'widgets/level_progress_widget.dart';
|
||||
import 'widgets/today_chart_widget.dart';
|
||||
import 'widgets/weekly_chart_widget.dart';
|
||||
import 'widgets/monthly_chart_widget.dart';
|
||||
|
||||
class StatisticsDashboard extends StatelessWidget {
|
||||
StatisticsDashboard({super.key});
|
||||
|
||||
final GamificationController gamController =
|
||||
Get.put(GamificationController());
|
||||
final StatisticsController statsController = Get.put(StatisticsController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
body: GetBuilder<GamificationController>(
|
||||
builder: (gc) {
|
||||
return GetBuilder<StatisticsController>(
|
||||
builder: (sc) {
|
||||
if (gc.isLoading || sc.isLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: FinanceDesignSystem.primaryDark),
|
||||
);
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// ═══════ App Bar ═══════
|
||||
SliverAppBar(
|
||||
expandedHeight: 200,
|
||||
pinned: true,
|
||||
stretch: true,
|
||||
backgroundColor: Get.isDarkMode
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: const Color(0xFF0A0E21),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded,
|
||||
color: Colors.white, size: 20),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded,
|
||||
color: Colors.white),
|
||||
onPressed: () => gc.fetchGamificationData(),
|
||||
),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Statistics'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: FinanceDesignSystem.balanceGradient,
|
||||
),
|
||||
),
|
||||
// أيقونة زخرفية
|
||||
Positioned(
|
||||
right: -40,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
Icons.bar_chart_rounded,
|
||||
size: 200,
|
||||
color: Colors.white.withOpacity(0.04),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -30,
|
||||
bottom: 20,
|
||||
child: Icon(
|
||||
Icons.emoji_events_rounded,
|
||||
size: 120,
|
||||
color: Colors.white.withOpacity(0.03),
|
||||
),
|
||||
),
|
||||
// بطاقة المستوى في الهيدر
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
gc.currentLevel.emoji,
|
||||
style: const TextStyle(fontSize: 40),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
Get.locale?.languageCode == 'ar'
|
||||
? gc.currentLevel.nameAr
|
||||
: gc.currentLevel.nameEn,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${gc.unlockedCount}/${gc.totalAchievements} ${'Achievements'.tr}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ═══════ المحتوى ═══════
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// 1. بطاقات الإحصائيات الأربعة
|
||||
_buildSummaryCards(gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 2. الهدف اليومي
|
||||
DailyGoalWidget(controller: gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 3. تقدم المستوى
|
||||
LevelProgressWidget(controller: gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 4. إحصائيات اليوم
|
||||
TodayChartWidget(),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 5. الرسم البياني الأسبوعي
|
||||
const WeeklyChartWidget(),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 6. التقرير الشهري
|
||||
const MonthlyChartWidget(),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 7. تقييم سلوك القيادة
|
||||
_buildBehaviorSection(gc),
|
||||
const SizedBox(
|
||||
height: FinanceDesignSystem.verticalSectionPadding),
|
||||
|
||||
// 8. الإنجازات
|
||||
_buildAchievementsSection(gc),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ بطاقات الإحصائيات ═══════
|
||||
Widget _buildSummaryCards(GamificationController gc) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
StatSummaryCard(
|
||||
icon: Icons.local_taxi_rounded,
|
||||
label: 'Total Trips'.tr,
|
||||
value: gc.totalTrips.toString(),
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
StatSummaryCard(
|
||||
icon: Icons.star_rounded,
|
||||
label: 'Rating'.tr,
|
||||
value: gc.averageRating.toStringAsFixed(1),
|
||||
color: const Color(0xFFFFD700),
|
||||
),
|
||||
StatSummaryCard(
|
||||
icon: Icons.whatshot_rounded,
|
||||
label: 'Day Streak'.tr,
|
||||
value: '${gc.consecutiveDays}',
|
||||
color: const Color(0xFFFF5722),
|
||||
),
|
||||
StatSummaryCard(
|
||||
icon: Icons.people_rounded,
|
||||
label: 'Referrals'.tr,
|
||||
value: gc.totalReferrals.toString(),
|
||||
color: const Color(0xFF9C27B0),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ قسم تقييم السلوك ═══════
|
||||
Widget _buildBehaviorSection(GamificationController gc) {
|
||||
bool isExcellent = gc.behaviorScore >= 90;
|
||||
bool isGood = gc.behaviorScore >= 75 && gc.behaviorScore < 90;
|
||||
|
||||
Color statusColor = isExcellent
|
||||
? Colors.green
|
||||
: isGood
|
||||
? Colors.orange
|
||||
: Colors.red;
|
||||
|
||||
String statusText = isExcellent
|
||||
? 'Excellent'.tr
|
||||
: isGood
|
||||
? 'Good'.tr
|
||||
: 'Needs Improvement'.tr;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.shield_rounded,
|
||||
color: FinanceDesignSystem.accentBlue),
|
||||
const SizedBox(width: 8),
|
||||
Text('Driving Behavior'.tr,
|
||||
style: FinanceDesignSystem.headingStyle),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_behaviorStatItem(
|
||||
title: 'Score'.tr,
|
||||
value: '${gc.behaviorScore}%',
|
||||
icon: Icons.speed_rounded,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
_behaviorStatItem(
|
||||
title: 'Max Speed'.tr,
|
||||
value: '${gc.maxSpeed.toStringAsFixed(0)} km/h',
|
||||
icon: Icons.directions_car_rounded,
|
||||
color: Colors.orange,
|
||||
),
|
||||
_behaviorStatItem(
|
||||
title: 'Hard Brakes'.tr,
|
||||
value: '${gc.hardBrakes}',
|
||||
icon: Icons.warning_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: gc.behaviorScore / 100,
|
||||
backgroundColor: FinanceDesignSystem.backgroundColor,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(statusColor),
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _behaviorStatItem(
|
||||
{required String title,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color}) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: color, size: 24),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
Text(title,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: FinanceDesignSystem.textSecondary)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════ قسم الإنجازات ═══════
|
||||
Widget _buildAchievementsSection(GamificationController gc) {
|
||||
final unlockedAch = gc.achievements.where((a) => a.isUnlocked).toList();
|
||||
final lockedAch = gc.achievements.where((a) => !a.isUnlocked).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Achievements'.tr, style: FinanceDesignSystem.headingStyle),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${gc.unlockedCount}/${gc.totalAchievements}',
|
||||
style: TextStyle(
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// الإنجازات المفتوحة
|
||||
if (unlockedAch.isNotEmpty) ...[
|
||||
...unlockedAch.map((a) => _buildAchievementCard(a, true)),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// الإنجازات المقفلة
|
||||
...lockedAch.map((a) => _buildAchievementCard(a, false)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAchievementCard(Achievement ach, bool unlocked) {
|
||||
final isAr = Get.locale?.languageCode == 'ar';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: unlocked ? Colors.white : Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
border: unlocked
|
||||
? Border.all(color: ach.color.withOpacity(0.3), width: 1.5)
|
||||
: null,
|
||||
boxShadow: unlocked
|
||||
? [
|
||||
BoxShadow(
|
||||
color: ach.color.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// أيقونة الإنجاز
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
unlocked ? ach.color.withOpacity(0.15) : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(
|
||||
ach.icon,
|
||||
color: unlocked ? ach.color : Colors.grey.shade400,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// المعلومات
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isAr ? ach.titleAr : ach.titleEn,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: unlocked
|
||||
? FinanceDesignSystem.primaryDark
|
||||
: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
isAr ? ach.descriptionAr : ach.descriptionEn,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
if (!unlocked) ...[
|
||||
const SizedBox(height: 8),
|
||||
// شريط التقدم
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
FractionallySizedBox(
|
||||
widthFactor: ach.progress,
|
||||
child: Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: ach.color.withOpacity(0.6),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${ach.currentProgress}/${ach.target}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey.shade400,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// أيقونة الحالة
|
||||
if (unlocked)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: ach.color.withOpacity(0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_rounded,
|
||||
color: ach.color,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
Icons.lock_outline_rounded,
|
||||
color: Colors.grey.shade300,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/gamification/gamification_controller.dart';
|
||||
|
||||
class DailyGoalWidget extends StatelessWidget {
|
||||
final GamificationController controller;
|
||||
|
||||
const DailyGoalWidget({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen.withOpacity(0.1)
|
||||
: FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
controller.isDailyGoalMet
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.track_changes_rounded,
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Daily Goal'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Daily Goal'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// زر تعديل الهدف
|
||||
InkWell(
|
||||
onTap: () => _showSetGoalDialog(context),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.dailyGoal > 0 ? 'Edit'.tr : 'Set Goal'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// مبلغ الأرباح والهدف
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
controller.dailyEarnings.toStringAsFixed(0),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.primaryDark,
|
||||
fontFamily: 'digit',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'/ ${controller.dailyGoal.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: FinanceDesignSystem.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// شريط التقدم
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 12,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOutCubic,
|
||||
height: 12,
|
||||
width: constraints.maxWidth * controller.dailyGoalProgress,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: controller.isDailyGoalMet
|
||||
? [
|
||||
FinanceDesignSystem.successGreen,
|
||||
const Color(0xFF69F0AE)
|
||||
]
|
||||
: [
|
||||
FinanceDesignSystem.accentBlue,
|
||||
const Color(0xFF82B1FF)
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue)
|
||||
.withOpacity(0.3),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${(controller.dailyGoalProgress * 100).toStringAsFixed(0)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: controller.isDailyGoalMet
|
||||
? FinanceDesignSystem.successGreen
|
||||
: FinanceDesignSystem.accentBlue,
|
||||
),
|
||||
),
|
||||
if (controller.isDailyGoalMet)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.celebration_rounded,
|
||||
color: Color(0xFFFFD700), size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Goal Achieved!'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.successGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'${'Remaining:'.tr} ${(controller.dailyGoal - controller.dailyEarnings).clamp(0, double.infinity).toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSetGoalDialog(BuildContext context) {
|
||||
final textController = TextEditingController(
|
||||
text: controller.dailyGoal > 0
|
||||
? controller.dailyGoal.toStringAsFixed(0)
|
||||
: '',
|
||||
);
|
||||
|
||||
Get.defaultDialog(
|
||||
backgroundColor: FinanceDesignSystem.cardColor,
|
||||
title: 'Set Daily Goal'.tr,
|
||||
titleStyle: FinanceDesignSystem.headingStyle,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
'How much do you want to earn today?'.tr,
|
||||
style: FinanceDesignSystem.subHeadingStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: textController,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'digit',
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: '5000',
|
||||
suffixText: 'SYP'.tr,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: FinanceDesignSystem.borderColor),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: FinanceDesignSystem.accentBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// الأهداف السريعة
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [1000, 3000, 5000, 10000].map((goal) {
|
||||
return ActionChip(
|
||||
label: Text('$goal'),
|
||||
labelStyle: const TextStyle(fontSize: 12),
|
||||
backgroundColor:
|
||||
FinanceDesignSystem.accentBlue.withOpacity(0.1),
|
||||
side: BorderSide.none,
|
||||
onPressed: () {
|
||||
textController.text = goal.toString();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () {
|
||||
double? goal = double.tryParse(textController.text);
|
||||
if (goal != null && goal > 0) {
|
||||
controller.setDailyGoal(goal);
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: FinanceDesignSystem.accentBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
),
|
||||
child: Text('Save'.tr),
|
||||
),
|
||||
cancel: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/gamification/gamification_controller.dart';
|
||||
|
||||
class LevelProgressWidget extends StatelessWidget {
|
||||
final GamificationController controller;
|
||||
const LevelProgressWidget({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final level = controller.currentLevel;
|
||||
final next = controller.nextLevel;
|
||||
final isAr = Get.locale?.languageCode == 'ar';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
level.color.withOpacity(0.08),
|
||||
level.gradientEnd.withOpacity(0.04)
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
border: Border.all(color: level.color.withOpacity(0.2), width: 1.5),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Driver Level'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
gradient:
|
||||
LinearGradient(colors: [level.color, level.gradientEnd]),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(level.emoji, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: 6),
|
||||
Text(isAr ? level.nameAr : level.nameEn,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13)),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: List.generate(DriverLevels.all.length, (i) {
|
||||
final lvl = DriverLevels.all[i];
|
||||
final isCurrent = lvl.id == level.id;
|
||||
final isPast = DriverLevels.all.indexOf(level) > i;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrent
|
||||
? lvl.color
|
||||
: isPast
|
||||
? lvl.color.withOpacity(0.6)
|
||||
: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
));
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: DriverLevels.all
|
||||
.map((l) => Expanded(
|
||||
child: Text(l.emoji,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(fontSize: l.id == level.id ? 16 : 12))))
|
||||
.toList()),
|
||||
const SizedBox(height: 16),
|
||||
if (next != null) ...[
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text('${'Next Level:'.tr} ${isAr ? next.nameAr : next.nameEn}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: FinanceDesignSystem.textSecondary)),
|
||||
Text('${(controller.progressToNext * 100).toStringAsFixed(0)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: level.color)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Stack(children: [
|
||||
Container(
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(5))),
|
||||
LayoutBuilder(
|
||||
builder: (ctx, c) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOutCubic,
|
||||
height: 10,
|
||||
width: c.maxWidth * controller.progressToNext,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [level.color, level.gradientEnd]),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
)),
|
||||
]),
|
||||
const SizedBox(height: 6),
|
||||
Text('${controller.totalPoints} / ${next.minPoints} ${'Points'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textMuted,
|
||||
fontFamily: 'digit')),
|
||||
] else
|
||||
Center(
|
||||
child: Text('🏆 ${'Maximum Level Reached!'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: level.color))),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: level.perks
|
||||
.map((p) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: level.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(p.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: level.color.withOpacity(0.8))),
|
||||
))
|
||||
.toList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/home/statistics/statistics_controller.dart';
|
||||
|
||||
class MonthlyChartWidget extends StatelessWidget {
|
||||
const MonthlyChartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<StatisticsController>(
|
||||
builder: (sc) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child:
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('Monthly Report'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 16),
|
||||
// Summary Row
|
||||
Row(children: [
|
||||
_summaryTile(
|
||||
'Total Earnings'.tr,
|
||||
'${sc.monthlyTotalEarnings.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
FinanceDesignSystem.successGreen),
|
||||
const SizedBox(width: 12),
|
||||
_summaryTile('Total Trips'.tr, '${sc.monthlyTotalTrips}',
|
||||
FinanceDesignSystem.accentBlue),
|
||||
const SizedBox(width: 12),
|
||||
_summaryTile(
|
||||
'Best Day'.tr, '${sc.bestDay}', const Color(0xFFFFD700)),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
// Monthly Earnings Line Chart
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: sc.monthlyEarnings.isEmpty
|
||||
? Center(
|
||||
child: Text('No data yet'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade400)))
|
||||
: LineChart(LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (spots) => spots
|
||||
.map((s) => LineTooltipItem(
|
||||
'${s.y.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (v) => FlLine(
|
||||
color: FinanceDesignSystem.borderColor,
|
||||
strokeWidth: 1),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 5,
|
||||
getTitlesWidget: (v, m) => Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text('${v.toInt()}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: FinanceDesignSystem.textSecondary)),
|
||||
),
|
||||
)),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: sc.monthlyEarnings
|
||||
.map((e) =>
|
||||
FlSpot(e.day.toDouble(), e.pricePerDay))
|
||||
.toList(),
|
||||
isCurved: true,
|
||||
curveSmoothness: 0.3,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
barWidth: 3,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (s, p, d, i) => FlDotCirclePainter(
|
||||
radius: 3,
|
||||
color: FinanceDesignSystem.accentBlue,
|
||||
strokeWidth: 1,
|
||||
strokeColor: Colors.white),
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
FinanceDesignSystem.accentBlue.withOpacity(0.2),
|
||||
FinanceDesignSystem.accentBlue.withOpacity(0.0)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _summaryTile(String label, String value, Color color) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Column(children: [
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w800, color: color)),
|
||||
const SizedBox(height: 2),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 9, color: FinanceDesignSystem.textSecondary),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class StatSummaryCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
|
||||
const StatSummaryCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
Icon(
|
||||
Icons.trending_up_rounded,
|
||||
color: Colors.grey.shade300,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
fontFamily: 'digit',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: FinanceDesignSystem.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
|
||||
class TodayChartWidget extends StatelessWidget {
|
||||
TodayChartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<HomeCaptainController>(
|
||||
builder: (hc) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Today Overview'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Summary of your daily activity'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: FinanceDesignSystem.textSecondary)),
|
||||
const SizedBox(height: 20),
|
||||
_buildRow(
|
||||
Icons.monetization_on_rounded,
|
||||
'Earnings'.tr,
|
||||
'${hc.totalMoneyToday} ${'SYP'.tr}',
|
||||
FinanceDesignSystem.successGreen),
|
||||
const Divider(height: 24),
|
||||
_buildRow(Icons.local_taxi_rounded, 'Rides'.tr, hc.countRideToday,
|
||||
FinanceDesignSystem.accentBlue),
|
||||
const Divider(height: 24),
|
||||
Obx(() => _buildRow(Icons.timer_rounded, 'Online Duration'.tr,
|
||||
hc.totalDurationDisplay.value, const Color(0xFFFF9800))),
|
||||
const Divider(height: 24),
|
||||
_buildRow(Icons.cancel_outlined, 'Refused'.tr, hc.countRefuse,
|
||||
FinanceDesignSystem.dangerRed),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(IconData icon, String label, String value, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: FinanceDesignSystem.primaryDark))),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: FinanceDesignSystem.primaryDark,
|
||||
fontFamily: 'digit')),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../constant/finance_design_system.dart';
|
||||
import '../../../../controller/home/statistics/statistics_controller.dart';
|
||||
|
||||
class WeeklyChartWidget extends StatelessWidget {
|
||||
const WeeklyChartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<StatisticsController>(
|
||||
builder: (sc) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.cardColor,
|
||||
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Text('Weekly Earnings'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: FinanceDesignSystem.successGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(
|
||||
'${sc.weeklyEarnings.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.successGreen)),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
_miniStat(Icons.local_taxi_rounded, '${sc.weeklyTrips}',
|
||||
'Rides'.tr, FinanceDesignSystem.accentBlue),
|
||||
const SizedBox(width: 16),
|
||||
_miniStat(
|
||||
Icons.timer_rounded,
|
||||
'${sc.weeklyHours.toStringAsFixed(1)}h',
|
||||
'Hours'.tr,
|
||||
const Color(0xFFFF9800)),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 180,
|
||||
child: sc.weeklyStats.isEmpty
|
||||
? Center(
|
||||
child: Text('No data yet'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade400)))
|
||||
: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: _getMaxY(sc.weeklyStats),
|
||||
barTouchData: BarTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipItem: (group, gi, rod, ri) =>
|
||||
BarTooltipItem(
|
||||
'${rod.toY.toStringAsFixed(0)} ${'SYP'.tr}',
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (v, m) {
|
||||
final idx = v.toInt();
|
||||
if (idx >= 0 &&
|
||||
idx < sc.weeklyStats.length) {
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
sc.weeklyStats[idx].dayName.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: FinanceDesignSystem
|
||||
.textSecondary)));
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
})),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
gridData: const FlGridData(show: false),
|
||||
barGroups: List.generate(sc.weeklyStats.length, (i) {
|
||||
final stat = sc.weeklyStats[i];
|
||||
final isToday = i == sc.weeklyStats.length - 1;
|
||||
return BarChartGroupData(x: i, barRods: [
|
||||
BarChartRodData(
|
||||
toY: stat.earnings,
|
||||
width: 20,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(6)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: isToday
|
||||
? [
|
||||
FinanceDesignSystem.accentBlue,
|
||||
const Color(0xFF82B1FF)
|
||||
]
|
||||
: [
|
||||
FinanceDesignSystem.primaryDark
|
||||
.withOpacity(0.6),
|
||||
FinanceDesignSystem.primaryDark
|
||||
.withOpacity(0.3)
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _miniStat(IconData icon, String value, String label, Color color) {
|
||||
return Row(children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: FinanceDesignSystem.primaryDark)),
|
||||
const SizedBox(width: 4),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: FinanceDesignSystem.textSecondary)),
|
||||
]);
|
||||
}
|
||||
|
||||
double _getMaxY(List<DayStat> stats) {
|
||||
if (stats.isEmpty) return 100;
|
||||
final max = stats.map((s) => s.earnings).reduce((a, b) => a > b ? a : b);
|
||||
return max <= 0 ? 100 : max * 1.2;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user