Initial commit for intaleq_admin
This commit is contained in:
2
.env
2
.env
@@ -61,7 +61,7 @@ emailService=seferservice@gmail.com
|
|||||||
allowed=TripzDriver:
|
allowed=TripzDriver:
|
||||||
allowedWallet=TripzWallet:
|
allowedWallet=TripzWallet:
|
||||||
passnpassenger=hbgbitbXrXrBr
|
passnpassenger=hbgbitbXrXrBr
|
||||||
ALLOWED_ADMIN_PHONES=962798583052,962790849027,962787021927
|
ALLOWED_ADMIN_PHONES=963992952235,962790849027,962787021927,963942542053,962772735902,971529155811
|
||||||
newId=new
|
newId=new
|
||||||
a=q
|
a=q
|
||||||
b=x
|
b=x
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.intaleq.intaleq_admin"
|
namespace = "com.intaleq.intaleq_admin"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path "src/main/cpp/CMakeLists.txt"
|
path "src/main/cpp/CMakeLists.txt"
|
||||||
version "3.31.5" // Match cmake_minimum_required in CMakeLists.txt
|
version "3.22.1" // Match cmake_minimum_required in CMakeLists.txt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" // Keep these!
|
abiFilters "armeabi-v7a", "arm64-v8a" // Keep these!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -43,10 +43,10 @@ android {
|
|||||||
applicationId = "com.intaleq.intaleq_admin"
|
applicationId = "com.intaleq.intaleq_admin"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = 23
|
minSdk = 30
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 5
|
||||||
versionName = '1.0.0'
|
versionName = '1.0.5'
|
||||||
multiDexEnabled =true
|
multiDexEnabled =true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +75,8 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:33.3.0') // مثال حديث
|
||||||
|
|
||||||
implementation 'com.scottyab:rootbeer-lib:0.1.0'
|
implementation 'com.scottyab:rootbeer-lib:0.1.0'
|
||||||
implementation 'com.google.android.gms:play-services-safetynet:18.0.1'
|
implementation 'com.google.android.gms:play-services-safetynet:18.0.1'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||||
|
|||||||
@@ -1,8 +1,31 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- Internet permission (usually not needed explicitly for Flutter, but safe to include) -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<!-- Fine location (already present) -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Camera -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<!-- Read/write external storage (for accessing photos) -->
|
||||||
|
<!-- On Android 10+ (API 29+), scoped storage reduces need for WRITE_EXTERNAL_STORAGE -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32" />
|
||||||
|
<!-- Optional: only needed if writing images (e.g., saving captured photos) on older Android -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="intaleq_admin"
|
android:label="intaleq_admin"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:fullBackupContent="false"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:usesCleartextTraffic="false"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -12,34 +35,48 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
|
||||||
the Android process has started. This theme is visible to the user
|
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
|
||||||
to determine the Window background behind the Flutter UI. -->
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<activity
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
android:name="com.yalantis.ucrop.UCropActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
|
||||||
https://developer.android.com/training/package-visibility and
|
|
||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
|
||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
<!-- Package visibility: allow interaction with camera & gallery apps on Android 11+ -->
|
||||||
<queries>
|
<queries>
|
||||||
|
<!-- For text processing (already present) -->
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent>
|
</intent>
|
||||||
|
|
||||||
|
<!-- Allow launching camera apps -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||||
|
</intent>
|
||||||
|
|
||||||
|
<!-- Allow launching gallery/photo picker apps -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.GET_CONTENT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent>
|
||||||
|
|
||||||
|
<!-- Optional: for opening image viewers or file managers -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.OPEN_DOCUMENT" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
21
android/app/src/main/res/xml/network_security_config.xml
Normal file
21
android/app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="false">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
|
||||||
|
<domain-config cleartextTrafficPermitted="false">
|
||||||
|
<domain includeSubdomains="true">intaleq.xyz</domain>
|
||||||
|
|
||||||
|
<pin-set expiration="2027-01-01">
|
||||||
|
<!-- <pin digest="SHA-256">pXmP2hTQLxDEvlTVmP5N7xpiA32sycBsxB6hBFT2uL4=</pin> -->
|
||||||
|
<pin digest="SHA-256">XJXX7XthMj5VlSHfvo1q73sY7orJ9Wle0X4avj0/Vwo=</pin>
|
||||||
|
|
||||||
|
<pin digest="SHA-256">C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHESsl=</pin>
|
||||||
|
</pin-set>
|
||||||
|
</domain-config>
|
||||||
|
|
||||||
|
|
||||||
|
</network-security-config>
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,9 @@
|
|||||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx4096M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
android.nonFinalResIds=true
|
||||||
|
dart.obfuscation=true
|
||||||
|
android.enableR8.fullMode=true
|
||||||
|
org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ PODS:
|
|||||||
- TOCropViewController (~> 2.7.4)
|
- TOCropViewController (~> 2.7.4)
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- IOSSecuritySuite (1.9.11)
|
||||||
|
- jailbreak_root_detection (1.0.1):
|
||||||
|
- Flutter
|
||||||
|
- IOSSecuritySuite (~> 1.9.10)
|
||||||
- libwebp (1.5.0):
|
- libwebp (1.5.0):
|
||||||
- libwebp/demux (= 1.5.0)
|
- libwebp/demux (= 1.5.0)
|
||||||
- libwebp/mux (= 1.5.0)
|
- libwebp/mux (= 1.5.0)
|
||||||
@@ -183,6 +187,7 @@ DEPENDENCIES:
|
|||||||
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
|
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
|
||||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- jailbreak_root_detection (from `.symlinks/plugins/jailbreak_root_detection/ios`)
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
@@ -206,6 +211,7 @@ SPEC REPOS:
|
|||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- GTMAppAuth
|
- GTMAppAuth
|
||||||
- GTMSessionFetcher
|
- GTMSessionFetcher
|
||||||
|
- IOSSecuritySuite
|
||||||
- libwebp
|
- libwebp
|
||||||
- Mantle
|
- Mantle
|
||||||
- nanopb
|
- nanopb
|
||||||
@@ -236,6 +242,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/image_cropper/ios"
|
:path: ".symlinks/plugins/image_cropper/ios"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
jailbreak_root_detection:
|
||||||
|
:path: ".symlinks/plugins/jailbreak_root_detection/ios"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
@@ -272,6 +280,8 @@ SPEC CHECKSUMS:
|
|||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
image_cropper: 5f162dcf988100dc1513f9c6b7eb42cd6fbf9156
|
image_cropper: 5f162dcf988100dc1513f9c6b7eb42cd6fbf9156
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||||
|
IOSSecuritySuite: b51056d5411aee567153ca86ce7f6edfdc5d2654
|
||||||
|
jailbreak_root_detection: 9201e1dfd51dc23069cbfb8d4f4a2d18305170bf
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
||||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ class AppLink {
|
|||||||
// static final String endPoint = box.read(BoxName.serverChosen);
|
// static final String endPoint = box.read(BoxName.serverChosen);
|
||||||
// static final String server = Env.seferCairoServer;
|
// static final String server = Env.seferCairoServer;
|
||||||
|
|
||||||
static final String server = 'https://intaleq.xyz/intaleq';
|
static final String server = 'https://api.intaleq.xyz/intaleq';
|
||||||
static String loginJwtDriver = "https://intaleq.xyz/intaleq/loginAdmin.php";
|
static String loginJwtDriver =
|
||||||
|
"https://api.intaleq.xyz/intaleq/loginAdmin.php";
|
||||||
|
|
||||||
static String googleMapsLink = 'https://maps.googleapis.com/maps/api/';
|
static String googleMapsLink = 'https://maps.googleapis.com/maps/api/';
|
||||||
static String llama = 'https://api.llama-api.com/chat/completions';
|
static String llama = 'https://api.llama-api.com/chat/completions';
|
||||||
@@ -41,6 +42,10 @@ class AppLink {
|
|||||||
"$seferPaymentServer/Admin/getPaymentsDashboard.php";
|
"$seferPaymentServer/Admin/getPaymentsDashboard.php";
|
||||||
static String getSeferWallet = "$tripzPaymentServer/seferWallet/get.php";
|
static String getSeferWallet = "$tripzPaymentServer/seferWallet/get.php";
|
||||||
static String addDrivePayment = "$tripzPaymentServer/payment/add.php";
|
static String addDrivePayment = "$tripzPaymentServer/payment/add.php";
|
||||||
|
static String addFromAdmin =
|
||||||
|
"$tripzPaymentServer/driverWallet/addFromAdmin.php";
|
||||||
|
static String add300ToDriver =
|
||||||
|
"$tripzPaymentServer/driverWallet/add300ToDriver.php";
|
||||||
static String updatePaymetToPaid =
|
static String updatePaymetToPaid =
|
||||||
"$tripzPaymentServer/payment/updatePaymetToPaid.php";
|
"$tripzPaymentServer/payment/updatePaymetToPaid.php";
|
||||||
static String wallet = '$tripzPaymentServer/passengerWallet';
|
static String wallet = '$tripzPaymentServer/passengerWallet';
|
||||||
@@ -238,7 +243,26 @@ class AppLink {
|
|||||||
static String getPassengerDetailsByPassengerID =
|
static String getPassengerDetailsByPassengerID =
|
||||||
"$server/Admin/getPassengerDetailsByPassengerID.php";
|
"$server/Admin/getPassengerDetailsByPassengerID.php";
|
||||||
static String getPassengerDetails = "$server/Admin/getPassengerDetails.php";
|
static String getPassengerDetails = "$server/Admin/getPassengerDetails.php";
|
||||||
|
static String admin_delete_and_blacklist_passenger =
|
||||||
|
"$server/Admin/passenger/admin_delete_and_blacklist_passenger.php";
|
||||||
|
static String admin_update_passenger =
|
||||||
|
"$server/Admin/passenger/admin_update_passenger.php";
|
||||||
|
static String admin_unblacklist =
|
||||||
|
"$server/Admin/passenger/admin_unblacklist.php";
|
||||||
|
static String admin_get_rides_by_phone =
|
||||||
|
"$server/Admin/rides/admin_get_rides_by_phone.php";
|
||||||
|
static String admin_update_ride_status =
|
||||||
|
"$server/Admin/rides/admin_update_ride_status.php";
|
||||||
static String getPassengerbyEmail = "$server/Admin/getPassengerbyEmail.php";
|
static String getPassengerbyEmail = "$server/Admin/getPassengerbyEmail.php";
|
||||||
|
static String updateDriverFromAdmin =
|
||||||
|
"$server/Admin/driver/updateDriverFromAdmin.php";
|
||||||
|
static String find_driver_by_phone =
|
||||||
|
"$server/Admin/driver/find_driver_by_phone.php";
|
||||||
|
static String getDriversPending =
|
||||||
|
"$server/auth/syria/driver/drivers_pending_list.php";
|
||||||
|
static String getDriverDetails =
|
||||||
|
"$server/auth/syria/driver/driver_details.php";
|
||||||
|
static String deleteCaptain = "$server/Admin/driver/deleteCaptain.php";
|
||||||
static String addAdminUser = "$server/Admin/adminUser/add.php";
|
static String addAdminUser = "$server/Admin/adminUser/add.php";
|
||||||
static String getdashbord = "$server/Admin/dashbord.php";
|
static String getdashbord = "$server/Admin/dashbord.php";
|
||||||
static String getEmployee = "$server/Admin/employee/get.php";
|
static String getEmployee = "$server/Admin/employee/get.php";
|
||||||
@@ -262,6 +286,10 @@ class AppLink {
|
|||||||
static String getPassengersStatic = "$serviceApp/getPassengersStatic.php";
|
static String getPassengersStatic = "$serviceApp/getPassengersStatic.php";
|
||||||
static String getRidesStatic = "$serviceApp/getRidesStatic.php";
|
static String getRidesStatic = "$serviceApp/getRidesStatic.php";
|
||||||
static String getEmployeeStatic = "$serviceApp/getEmployeeStatic.php";
|
static String getEmployeeStatic = "$serviceApp/getEmployeeStatic.php";
|
||||||
|
static String getNotesForEmployee = "$serviceApp/getNotesForEmployee.php";
|
||||||
|
static String getEmployeeDriverAfterCallingRegister =
|
||||||
|
"$serviceApp/getEmployeeDriverAfterCallingRegister.php";
|
||||||
|
static String getEditorStatsCalls = "$serviceApp/getEditorStatsCalls.php";
|
||||||
static String getdriverstotalMonthly =
|
static String getdriverstotalMonthly =
|
||||||
"$serviceApp/getdriverstotalMonthly.php";
|
"$serviceApp/getdriverstotalMonthly.php";
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,42 @@ class CaptainAdminController extends GetxController {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future deletCaptain() async {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
var res = await CRUD().get(
|
||||||
|
link: AppLink.deleteCaptain,
|
||||||
|
payload: {},
|
||||||
|
);
|
||||||
|
var d = jsonDecode(res);
|
||||||
|
if (d['status'] == 'success') {
|
||||||
|
captainData = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future find_driver_by_phone(String phone) async {
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: AppLink.find_driver_by_phone,
|
||||||
|
payload: {'phone': phone},
|
||||||
|
);
|
||||||
|
var d = (res);
|
||||||
|
if (d != 'failure') {
|
||||||
|
captainData = d;
|
||||||
|
} else {
|
||||||
|
captainData = {};
|
||||||
|
Get.snackbar('Error', 'No captain found with this phone number',
|
||||||
|
backgroundColor: AppColor.redColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
Future addCaptainPrizeToWallet() async {
|
Future addCaptainPrizeToWallet() async {
|
||||||
String? paymentId;
|
String? paymentId;
|
||||||
//todo link to add wallet to captain
|
//todo link to add wallet to captain
|
||||||
|
|||||||
@@ -21,41 +21,47 @@ class DashboardController extends GetxController {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
update();
|
update();
|
||||||
|
|
||||||
// الطلب من السيرفر الرئيسي
|
// 🔹 Request main dashboard data
|
||||||
var res = await CRUD().get(link: AppLink.getdashbord, payload: {});
|
var res = await CRUD().get(link: AppLink.getdashbord, payload: {});
|
||||||
|
print('📡 Main dashboard response: $res');
|
||||||
|
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
var d = jsonDecode(res);
|
var d = jsonDecode(res);
|
||||||
// Log.print('d: ${d}');
|
print('✅ Decoded main dashboard: ${jsonEncode(d)}');
|
||||||
dashbord = d['message']; // هذا عبارة عن List<Map>
|
dashbord = d['message'];
|
||||||
|
} else {
|
||||||
|
print('❌ Failed to load main dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
// الطلب من سيرفر المحافظ
|
// 🔹 Request wallet dashboard data
|
||||||
var resPayments = await CRUD().postWallet(
|
var resPayments = await CRUD().postWallet(
|
||||||
link: AppLink.getPaymentsDashboard,
|
link: AppLink.getPaymentsDashboard,
|
||||||
payload: {},
|
payload: {},
|
||||||
);
|
);
|
||||||
|
print('💳 Wallet dashboard response: $resPayments');
|
||||||
|
|
||||||
if (resPayments != 'failure') {
|
if (resPayments != 'failure') {
|
||||||
var p = resPayments;
|
var p = resPayments;
|
||||||
// Log.print('p: ${p}');
|
print('✅ Decoded wallet dashboard: ${jsonEncode(p)}');
|
||||||
|
|
||||||
// نتأكد أن الكل Map بداخل List
|
|
||||||
if (dashbord.isNotEmpty &&
|
if (dashbord.isNotEmpty &&
|
||||||
p['message'] is List &&
|
p['message'] is List &&
|
||||||
p['message'].isNotEmpty) {
|
p['message'].isNotEmpty) {
|
||||||
dashbord[0].addAll(p['message'][0]); // ندمج المعلومات داخل نفس الـ Map
|
dashbord[0].addAll(p['message'][0]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
print('❌ Failed to load wallet dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
// كريدت الرسائل
|
// 🔹 Check SMS credit
|
||||||
var res2 = await CRUD().kazumiSMS(
|
var res2 = await CRUD().kazumiSMS(
|
||||||
link: 'https://sms.kazumi.me/api/sms/check-credit',
|
link: 'https://sms.kazumi.me/api/sms/check-credit',
|
||||||
payload: {"username": "Sefer", "password": AK.smsPasswordEgypt},
|
payload: {"username": "Sefer", "password": AK.smsPasswordEgypt},
|
||||||
);
|
);
|
||||||
|
|
||||||
creditSMS = res2['credit'];
|
creditSMS = res2['credit'];
|
||||||
Log.print(' res2[credit]: ${res2['credit']}');
|
print('📱 SMS Credit Response: ${jsonEncode(res2)}');
|
||||||
Log.print('creditSMS: ${creditSMS}');
|
print('💰 creditSMS: $creditSMS');
|
||||||
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
update();
|
update();
|
||||||
@@ -70,7 +76,7 @@ class DashboardController extends GetxController {
|
|||||||
// box.read(BoxName.tokensDrivers)['message'][i]['phone'].toString(),
|
// box.read(BoxName.tokensDrivers)['message'][i]['phone'].toString(),
|
||||||
smsText.text,
|
smsText.text,
|
||||||
);
|
);
|
||||||
// Log.print('CRUD().phoneDriversTest.: ${phoneNumber['phone']}');
|
// print('CRUD().phoneDriversTest.: ${phoneNumber['phone']}');
|
||||||
Future.delayed(const Duration(microseconds: 20));
|
Future.delayed(const Duration(microseconds: 20));
|
||||||
}
|
}
|
||||||
Get.back();
|
Get.back();
|
||||||
|
|||||||
@@ -43,6 +43,70 @@ class PassengerAdminController extends GetxController {
|
|||||||
|
|
||||||
Get.back();
|
Get.back();
|
||||||
}
|
}
|
||||||
|
// داخل الـController نفسه
|
||||||
|
|
||||||
|
Future<bool> updatePassenger({
|
||||||
|
required String id, // أو مرّر phoneLookup بدل id لو حاب
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
String? phone,
|
||||||
|
}) async {
|
||||||
|
// لا نرسل طلب إذا ما في أي تغيير
|
||||||
|
if ((firstName == null || firstName.trim().isEmpty) &&
|
||||||
|
(lastName == null || lastName.trim().isEmpty) &&
|
||||||
|
(phone == null || phone.trim().isEmpty)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// فلتر بسيط للأرقام فقط
|
||||||
|
// String _normalizePhone(String s) => s.replaceAll(RegExp(r'\D+'), '');
|
||||||
|
|
||||||
|
final Map<String, dynamic> payload = {
|
||||||
|
'id':
|
||||||
|
id, // لو بدك تستخدم phone_lookup بدل id: احذف هذا وأرسل {'phone_lookup': phoneLookup}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (firstName != null && firstName.trim().isNotEmpty) {
|
||||||
|
payload['first_name'] = firstName.trim();
|
||||||
|
}
|
||||||
|
if (lastName != null && lastName.trim().isNotEmpty) {
|
||||||
|
payload['last_name'] = lastName.trim();
|
||||||
|
}
|
||||||
|
if (phone != null && phone.trim().isNotEmpty) {
|
||||||
|
payload['phone'] = (phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// حالة تحميل
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await CRUD().post(
|
||||||
|
link: AppLink.admin_update_passenger, // عدّل الرابط حسب اسم مسارك
|
||||||
|
payload: payload,
|
||||||
|
);
|
||||||
|
final d = (res);
|
||||||
|
|
||||||
|
final ok = (d['status'] == 'success');
|
||||||
|
if (ok) {
|
||||||
|
// (اختياري) حدّث الكاش/الواجهة — مثلاً أعد الجلب
|
||||||
|
Get.snackbar('Update successful',
|
||||||
|
d['message']?.toString() ?? 'Passenger updated successfully',
|
||||||
|
backgroundColor: AppColor.greenColor);
|
||||||
|
// await getPassengerCount(); // أو حدّث passengersData محليًا إذا متاح
|
||||||
|
} else {
|
||||||
|
// (اختياري) أظهر رسالة خطأ
|
||||||
|
// Get.snackbar('Update failed', d['message']?.toString() ?? 'Unknown error');
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
} catch (e) {
|
||||||
|
// Get.snackbar('Error', e.toString());
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void addPassengerPrizeToWalletSecure() async {
|
void addPassengerPrizeToWalletSecure() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,238 +1,507 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
import '../../models/model/passengers_model.dart';
|
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
import '../functions/crud.dart';
|
import '../functions/crud.dart';
|
||||||
|
|
||||||
class StaticController extends GetxController {
|
class StaticController extends GetxController {
|
||||||
Map<String, dynamic> jsonData1 = {};
|
// --- Date & State Management ---
|
||||||
Map<String, dynamic> jsonData2 = {};
|
DateTime? startDate = DateTime(DateTime.now().year, DateTime.now().month, 1);
|
||||||
List staticList = [];
|
DateTime? endDate =
|
||||||
var chartDataPassengers;
|
DateTime(DateTime.now().year, DateTime.now().month + 1, 0);
|
||||||
var chartDataDrivers;
|
|
||||||
var chartDataDriversCalling;
|
DateTime? compareStartDate;
|
||||||
var chartDataRides;
|
DateTime? compareEndDate;
|
||||||
var chartDataEmployee;
|
|
||||||
var chartDataEmployeeMaryam;
|
bool isComparing = false;
|
||||||
var chartDataEmployeeRawda;
|
|
||||||
var chartDataEmployeeMena;
|
|
||||||
var chartDataEmployeeSefer4;
|
|
||||||
var chartDataDriversMatchingNotes;
|
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
String totalMonthlyPassengers = '';
|
|
||||||
String totalMonthlyRides = '';
|
|
||||||
String totalMonthlyEmployee = '';
|
|
||||||
String totalMonthlyDrivers = '';
|
|
||||||
late List<MonthlyPassengerInstall> passengersData;
|
|
||||||
late List<MonthlyRidesInstall> ridesData;
|
|
||||||
late List<MonthlyEmployeeData> employeeData;
|
|
||||||
late List<MonthlyDriverInstall> driversData;
|
|
||||||
|
|
||||||
Future<void> fetch() async {
|
// --- Daily Notes State ---
|
||||||
|
bool isLoadingNotes = false;
|
||||||
|
List<dynamic> dailyNotesList = [];
|
||||||
|
|
||||||
|
// --- Chart Data (Current Range) ---
|
||||||
|
List<FlSpot> chartDataPassengers = [];
|
||||||
|
List<FlSpot> chartDataDrivers = [];
|
||||||
|
List<FlSpot> chartDataRides = [];
|
||||||
|
List<FlSpot> chartDataDriversMatchingNotes = [];
|
||||||
|
|
||||||
|
// Employee Data (Notes/General Stats)
|
||||||
|
List<FlSpot> chartDataEmployeerama1 = [];
|
||||||
|
List<FlSpot> chartDataEmployeeshahd = [];
|
||||||
|
List<FlSpot> chartDataEmployeeRama2 = [];
|
||||||
|
List<FlSpot> chartDataEmployeeSefer4 = [];
|
||||||
|
|
||||||
|
// Employee Data (Calls/Activations Stats)
|
||||||
|
List<FlSpot> chartDataCallsrama1 = [];
|
||||||
|
List<FlSpot> chartDataCallsShahd = [];
|
||||||
|
List<FlSpot> chartDataCallsRama2 = [];
|
||||||
|
List<FlSpot> chartDataCallsSefer4 = [];
|
||||||
|
|
||||||
|
// --- Chart Data (Comparison Range) ---
|
||||||
|
List<FlSpot> chartDataPassengersCompare = [];
|
||||||
|
List<FlSpot> chartDataDriversCompare = [];
|
||||||
|
List<FlSpot> chartDataRidesCompare = [];
|
||||||
|
List<FlSpot> chartDataDriversMatchingNotesCompare = [];
|
||||||
|
|
||||||
|
// Employee Comparison (Notes)
|
||||||
|
List<FlSpot> chartDataEmployeerama1Compare = [];
|
||||||
|
List<FlSpot> chartDataEmployeeshahdCompare = [];
|
||||||
|
List<FlSpot> chartDataEmployeeRama2Compare = [];
|
||||||
|
List<FlSpot> chartDataEmployeeSefer4Compare = [];
|
||||||
|
|
||||||
|
// Employee Comparison (Calls/Activations)
|
||||||
|
List<FlSpot> chartDataCallsrama1Compare = [];
|
||||||
|
List<FlSpot> chartDataCallsShahdCompare = [];
|
||||||
|
List<FlSpot> chartDataCallsRama2Compare = [];
|
||||||
|
List<FlSpot> chartDataCallsSefer4Compare = [];
|
||||||
|
|
||||||
|
// --- Totals ---
|
||||||
|
String totalMonthlyPassengers = '0';
|
||||||
|
String totalMonthlyRides = '0';
|
||||||
|
String totalMonthlyDrivers = '0';
|
||||||
|
|
||||||
|
// --- Raw Lists ---
|
||||||
|
List staticList = [];
|
||||||
|
|
||||||
|
// --- Employment Type Stats List (Simple Count) ---
|
||||||
|
List<Map<String, dynamic>> employmentStatsList = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers for View ---
|
||||||
|
double get daysInPeriod {
|
||||||
|
if (startDate == null || endDate == null) return 31;
|
||||||
|
return endDate!.difference(startDate!).inDays + 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get currentDateString {
|
||||||
|
if (startDate == null || endDate == null) return "";
|
||||||
|
return "${DateFormat('yyyy-MM-dd').format(startDate!)} : ${DateFormat('yyyy-MM-dd').format(endDate!)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Date Actions ---
|
||||||
|
void updateDateRange(DateTime start, DateTime end) {
|
||||||
|
startDate = start;
|
||||||
|
endDate = end;
|
||||||
|
if (isComparing) _calculateCompareDates();
|
||||||
|
getAll();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateCompareDates() {
|
||||||
|
if (startDate == null || endDate == null) return;
|
||||||
|
Duration duration = endDate!.difference(startDate!);
|
||||||
|
compareEndDate = startDate!.subtract(const Duration(days: 1));
|
||||||
|
compareStartDate = compareEndDate!.subtract(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleComparison() async {
|
||||||
|
isComparing = !isComparing;
|
||||||
|
if (isComparing) {
|
||||||
|
_calculateCompareDates();
|
||||||
|
} else {
|
||||||
|
compareStartDate = null;
|
||||||
|
compareEndDate = null;
|
||||||
|
_clearComparisonData();
|
||||||
|
}
|
||||||
|
await getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearComparisonData() {
|
||||||
|
chartDataPassengersCompare.clear();
|
||||||
|
chartDataDriversCompare.clear();
|
||||||
|
chartDataRidesCompare.clear();
|
||||||
|
chartDataDriversMatchingNotesCompare.clear();
|
||||||
|
|
||||||
|
chartDataEmployeerama1Compare.clear();
|
||||||
|
chartDataEmployeeshahdCompare.clear();
|
||||||
|
chartDataEmployeeRama2Compare.clear();
|
||||||
|
chartDataEmployeeSefer4Compare.clear();
|
||||||
|
|
||||||
|
chartDataCallsrama1Compare.clear();
|
||||||
|
chartDataCallsShahdCompare.clear();
|
||||||
|
chartDataCallsRama2Compare.clear();
|
||||||
|
chartDataCallsSefer4Compare.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _getPayload(DateTime start, DateTime end) {
|
||||||
|
return {
|
||||||
|
"start_date": DateFormat('yyyy-MM-dd').format(start),
|
||||||
|
"end_date": DateFormat('yyyy-MM-dd').format(end),
|
||||||
|
"month": start.month.toString(),
|
||||||
|
"year": start.year.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main Fetch Logic ---
|
||||||
|
Future getAll() async {
|
||||||
|
if (startDate == null || endDate == null) return;
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
update(); // Notify the observers about the loading state change
|
update();
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
fetchPassengers(isCompare: false),
|
||||||
|
fetchRides(isCompare: false),
|
||||||
|
fetchDrivers(isCompare: false),
|
||||||
|
fetchEmployee(isCompare: false),
|
||||||
|
fetchEditorCalls(isCompare: false),
|
||||||
|
fetchEmploymentStats(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isComparing && compareStartDate != null && compareEndDate != null) {
|
||||||
|
await Future.wait([
|
||||||
|
fetchPassengers(isCompare: true),
|
||||||
|
fetchRides(isCompare: true),
|
||||||
|
fetchDrivers(isCompare: true),
|
||||||
|
fetchEmployee(isCompare: true),
|
||||||
|
fetchEditorCalls(isCompare: true),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
var res = await CRUD().get(
|
|
||||||
link: AppLink.getPassengersStatic,
|
|
||||||
payload: {},
|
|
||||||
);
|
|
||||||
jsonData1 = jsonDecode(res);
|
|
||||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... (Existing Functions _generateSpots, fetchPassengers, etc.) ...
|
||||||
|
List<FlSpot> _generateSpots(List<dynamic> data, String dateKey,
|
||||||
|
String valueKey, DateTime startOfRange) {
|
||||||
|
List<FlSpot> spots = [];
|
||||||
|
Map<String, double> dataMap = {};
|
||||||
|
for (var item in data) {
|
||||||
|
String dateStr = item[dateKey].toString();
|
||||||
|
double val = double.tryParse(item[valueKey].toString()) ?? 0.0;
|
||||||
|
dataMap[dateStr] = val;
|
||||||
|
}
|
||||||
|
DateTime rangeEnd =
|
||||||
|
(startOfRange == startDate) ? endDate! : compareEndDate!;
|
||||||
|
int totalDays = rangeEnd.difference(startOfRange).inDays + 1;
|
||||||
|
for (int i = 0; i < totalDays; i++) {
|
||||||
|
DateTime currentDate = startOfRange.add(Duration(days: i));
|
||||||
|
String dateKeyStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||||
|
double value = dataMap[dateKeyStr] ?? 0.0;
|
||||||
|
spots.add(FlSpot((i + 1).toDouble(), value));
|
||||||
|
}
|
||||||
|
return spots;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPassengers({bool isCompare = false}) async {
|
||||||
|
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||||
|
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||||
|
var res = await CRUD().get(
|
||||||
|
link: AppLink.getPassengersStatic, payload: _getPayload(start, end));
|
||||||
|
var jsonResponse = jsonDecode(res);
|
||||||
|
if (jsonResponse['status'] == 'failure') return;
|
||||||
final List<dynamic> jsonData = jsonResponse['message'];
|
final List<dynamic> jsonData = jsonResponse['message'];
|
||||||
|
if (!isCompare &&
|
||||||
|
jsonData.isNotEmpty &&
|
||||||
|
jsonData[0]['totalMonthly'] != null) {
|
||||||
totalMonthlyPassengers = jsonData[0]['totalMonthly'].toString();
|
totalMonthlyPassengers = jsonData[0]['totalMonthly'].toString();
|
||||||
passengersData = jsonData.map<MonthlyPassengerInstall>((item) {
|
}
|
||||||
return MonthlyPassengerInstall.fromJson(item);
|
List<FlSpot> spots =
|
||||||
}).toList();
|
_generateSpots(jsonData, 'day', 'totalPassengers', start);
|
||||||
final List<FlSpot> spots = passengersData
|
if (isCompare)
|
||||||
.map((data) => FlSpot(
|
chartDataPassengersCompare = spots;
|
||||||
data.day.toDouble(),
|
else
|
||||||
data.totalPassengers.toDouble(),
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
chartDataPassengers = spots;
|
chartDataPassengers = spots;
|
||||||
|
|
||||||
update(); // Notify the observers about the data and loading state change
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchRides() async {
|
Future<void> fetchRides({bool isCompare = false}) async {
|
||||||
isLoading = true;
|
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||||
update(); // Notify the observers about the loading state change
|
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||||
|
var res = await CRUD()
|
||||||
var res = await CRUD().get(
|
.get(link: AppLink.getRidesStatic, payload: _getPayload(start, end));
|
||||||
link: AppLink.getRidesStatic,
|
var jsonResponse = jsonDecode(res);
|
||||||
payload: {},
|
if (jsonResponse['status'] == 'failure') return;
|
||||||
);
|
|
||||||
jsonData1 = jsonDecode(res);
|
|
||||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
|
||||||
isLoading = false;
|
|
||||||
final List<dynamic> jsonData = jsonResponse['message'];
|
final List<dynamic> jsonData = jsonResponse['message'];
|
||||||
|
if (!isCompare &&
|
||||||
|
jsonData.isNotEmpty &&
|
||||||
|
jsonData[0]['totalMonthly'] != null) {
|
||||||
totalMonthlyRides = jsonData[0]['totalMonthly'].toString();
|
totalMonthlyRides = jsonData[0]['totalMonthly'].toString();
|
||||||
ridesData = jsonData.map<MonthlyRidesInstall>((item) {
|
}
|
||||||
return MonthlyRidesInstall.fromJson(item);
|
List<FlSpot> spots = _generateSpots(jsonData, 'day', 'totalRides', start);
|
||||||
}).toList();
|
if (isCompare)
|
||||||
final List<FlSpot> spots = ridesData
|
chartDataRidesCompare = spots;
|
||||||
.map((data) => FlSpot(
|
else
|
||||||
data.day.toDouble(),
|
|
||||||
data.totalRides.toDouble(),
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
chartDataRides = spots;
|
chartDataRides = spots;
|
||||||
|
|
||||||
update(); // Notify the observers about the data and loading state change
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchEmployee() async {
|
|
||||||
try {
|
|
||||||
isLoading = true;
|
|
||||||
update();
|
|
||||||
|
|
||||||
var res = await CRUD().get(link: AppLink.getEmployeeStatic, payload: {});
|
|
||||||
|
|
||||||
// First check if the response is valid JSON
|
|
||||||
if (res == 'failure') {
|
|
||||||
throw FormatException('Invalid response: $res');
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
|
||||||
|
|
||||||
// Initialize empty lists for all chart data
|
|
||||||
chartDataEmployeeMaryam = <FlSpot>[];
|
|
||||||
chartDataEmployeeRawda = <FlSpot>[];
|
|
||||||
chartDataEmployeeMena = <FlSpot>[];
|
|
||||||
chartDataEmployeeSefer4 = <FlSpot>[];
|
|
||||||
totalMonthlyRides = '0';
|
|
||||||
|
|
||||||
// Check for error response
|
|
||||||
if (jsonResponse['status'] == 'failure') {
|
|
||||||
isLoading = false;
|
|
||||||
update();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchDrivers({bool isCompare = false}) async {
|
||||||
|
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||||
|
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||||
|
var res = await CRUD().get(
|
||||||
|
link: AppLink.getdriverstotalMonthly, payload: _getPayload(start, end));
|
||||||
|
var jsonResponse = jsonDecode(res);
|
||||||
|
if (jsonResponse['status'] == 'failure') return;
|
||||||
final List<dynamic> jsonData = jsonResponse['message'];
|
final List<dynamic> jsonData = jsonResponse['message'];
|
||||||
if (jsonData.isEmpty) {
|
if (!isCompare &&
|
||||||
isLoading = false;
|
jsonData.isNotEmpty &&
|
||||||
update();
|
jsonData[0]['totalMonthlyDrivers'] != null) {
|
||||||
return;
|
totalMonthlyDrivers = jsonData[0]['totalMonthlyDrivers'].toString();
|
||||||
|
}
|
||||||
|
if (!isCompare) {
|
||||||
|
staticList = jsonData;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalMonthlyRides = jsonData[0]['totalMonthly']?.toString() ?? '0';
|
List<FlSpot> spotsDrivers =
|
||||||
|
_generateSpots(jsonData, 'day', 'dailyTotalDrivers', start);
|
||||||
|
List<FlSpot> spotsNotes =
|
||||||
|
_generateSpots(jsonData, 'day', 'dailyMatchingNotes', start);
|
||||||
|
if (isCompare) {
|
||||||
|
chartDataDriversCompare = spotsDrivers;
|
||||||
|
chartDataDriversMatchingNotesCompare = spotsNotes;
|
||||||
|
} else {
|
||||||
|
chartDataDrivers = spotsDrivers;
|
||||||
|
chartDataDriversMatchingNotes = spotsNotes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Group data by employee
|
Future<void> fetchEmployee({bool isCompare = false}) async {
|
||||||
Map<String, List<MonthlyEmployeeData>> employeeDataMap = {};
|
try {
|
||||||
|
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||||
|
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||||
|
var res = await CRUD().get(
|
||||||
|
link: AppLink.getEmployeeStatic, payload: _getPayload(start, end));
|
||||||
|
|
||||||
|
if (isCompare) {
|
||||||
|
chartDataEmployeerama1Compare = [];
|
||||||
|
chartDataEmployeeshahdCompare = [];
|
||||||
|
chartDataEmployeeRama2Compare = [];
|
||||||
|
chartDataEmployeeSefer4Compare = [];
|
||||||
|
} else {
|
||||||
|
chartDataEmployeerama1 = [];
|
||||||
|
chartDataEmployeeshahd = [];
|
||||||
|
chartDataEmployeeRama2 = [];
|
||||||
|
chartDataEmployeeSefer4 = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == 'failure') return;
|
||||||
|
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||||
|
if (jsonResponse['status'] == 'failure') return;
|
||||||
|
final List<dynamic> jsonData = jsonResponse['message'];
|
||||||
|
if (jsonData.isEmpty) return;
|
||||||
|
|
||||||
|
Map<String, Map<String, double>> dateNameMap = {};
|
||||||
for (var item in jsonData) {
|
for (var item in jsonData) {
|
||||||
var employeeData = MonthlyEmployeeData.fromJson(item);
|
String dateKeyStr = item['date'] ?? item['day'];
|
||||||
if (!employeeDataMap.containsKey(employeeData.name)) {
|
String name = item['NAME'].toString().toLowerCase().trim();
|
||||||
employeeDataMap[employeeData.name] = [];
|
double count = double.tryParse(item['count'].toString()) ?? 0.0;
|
||||||
|
if (!dateNameMap.containsKey(dateKeyStr)) dateNameMap[dateKeyStr] = {};
|
||||||
|
if (dateNameMap[dateKeyStr]!.containsKey(name)) {
|
||||||
|
dateNameMap[dateKeyStr]![name] =
|
||||||
|
dateNameMap[dateKeyStr]![name]! + count;
|
||||||
|
} else {
|
||||||
|
dateNameMap[dateKeyStr]![name] = count;
|
||||||
}
|
}
|
||||||
employeeDataMap[employeeData.name]!.add(employeeData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final today = DateTime.now().day;
|
final targetLists = isCompare
|
||||||
|
? {
|
||||||
// Create data for each employee
|
'rama1': chartDataEmployeerama1Compare,
|
||||||
final employeeNames = {
|
'shahd': chartDataEmployeeshahdCompare,
|
||||||
'maryam': chartDataEmployeeMaryam,
|
'rama2': chartDataEmployeeRama2Compare,
|
||||||
'yasmine': chartDataEmployeeRawda,
|
'mayar': chartDataEmployeeSefer4Compare
|
||||||
'mena': chartDataEmployeeMena,
|
}
|
||||||
'ashjan': chartDataEmployeeSefer4,
|
: {
|
||||||
|
'rama1': chartDataEmployeerama1,
|
||||||
|
'shahd': chartDataEmployeeshahd,
|
||||||
|
'rama2': chartDataEmployeeRama2,
|
||||||
|
'mayar': chartDataEmployeeSefer4
|
||||||
};
|
};
|
||||||
|
|
||||||
employeeNames.forEach((name, chartData) {
|
int totalDays = end.difference(start).inDays + 1;
|
||||||
var spots = <FlSpot>[];
|
targetLists.forEach((key, listToFill) {
|
||||||
for (int day = 1; day <= today; day++) {
|
for (int i = 0; i < totalDays; i++) {
|
||||||
spots.add(FlSpot(
|
DateTime currentDate = start.add(Duration(days: i));
|
||||||
day.toDouble(),
|
String currentDateStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||||
employeeDataMap[name]
|
double value = 0;
|
||||||
?.firstWhere(
|
Map<String, double>? dayData = dateNameMap[currentDateStr];
|
||||||
(e) => e.day == day,
|
if (dayData != null) {
|
||||||
orElse: () => MonthlyEmployeeData(
|
// if (key == 'mayar') {
|
||||||
day: day,
|
// value = (dayData['mayar'] ?? 0) +
|
||||||
totalEmployees: 0,
|
// (dayData['rama1'] ?? 0) +
|
||||||
name: name,
|
// (dayData['sefer4'] ?? 0);
|
||||||
),
|
// } else {
|
||||||
)
|
value = dayData[key] ?? 0;
|
||||||
.totalEmployees
|
// }
|
||||||
.toDouble() ??
|
}
|
||||||
0,
|
listToFill.add(FlSpot((i + 1).toDouble(), value));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly cast to List<FlSpot>
|
|
||||||
if (name == 'maryam')
|
|
||||||
chartDataEmployeeMaryam = List<FlSpot>.from(spots);
|
|
||||||
if (name == 'yasmine')
|
|
||||||
chartDataEmployeeRawda = List<FlSpot>.from(spots);
|
|
||||||
if (name == 'mena') chartDataEmployeeMena = List<FlSpot>.from(spots);
|
|
||||||
if (name == 'ashjan')
|
|
||||||
chartDataEmployeeSefer4 = List<FlSpot>.from(spots);
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.print('Error in fetchEmployee: $e');
|
Log.print('Error in fetchEmployee: $e');
|
||||||
// Set empty FlSpot lists in case of error
|
}
|
||||||
chartDataEmployeeMaryam = <FlSpot>[];
|
}
|
||||||
chartDataEmployeeRawda = <FlSpot>[];
|
|
||||||
chartDataEmployeeMena = <FlSpot>[];
|
Future<void> fetchEditorCalls({bool isCompare = false}) async {
|
||||||
chartDataEmployeeSefer4 = <FlSpot>[];
|
try {
|
||||||
totalMonthlyRides = '0';
|
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||||
|
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||||
|
|
||||||
|
var res = await CRUD().get(
|
||||||
|
link: AppLink.getEditorStatsCalls, payload: _getPayload(start, end));
|
||||||
|
|
||||||
|
if (isCompare) {
|
||||||
|
chartDataCallsrama1Compare = [];
|
||||||
|
chartDataCallsShahdCompare = [];
|
||||||
|
chartDataCallsRama2Compare = [];
|
||||||
|
chartDataCallsSefer4Compare = [];
|
||||||
|
} else {
|
||||||
|
chartDataCallsrama1 = [];
|
||||||
|
chartDataCallsShahd = [];
|
||||||
|
chartDataCallsRama2 = [];
|
||||||
|
chartDataCallsSefer4 = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res == 'failure') return;
|
||||||
|
|
||||||
|
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||||
|
if (jsonResponse['status'] == 'failure') return;
|
||||||
|
|
||||||
|
final List<dynamic> jsonData = jsonResponse['message'];
|
||||||
|
if (jsonData.isEmpty) return;
|
||||||
|
|
||||||
|
Map<String, Map<String, double>> dateNameMap = {};
|
||||||
|
|
||||||
|
for (var item in jsonData) {
|
||||||
|
String dateKeyStr = item['date'] ?? item['day'];
|
||||||
|
String name = item['NAME'].toString().toLowerCase().trim();
|
||||||
|
double count = double.tryParse(item['count'].toString()) ?? 0.0;
|
||||||
|
|
||||||
|
if (!dateNameMap.containsKey(dateKeyStr)) {
|
||||||
|
dateNameMap[dateKeyStr] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateNameMap[dateKeyStr]!.containsKey(name)) {
|
||||||
|
dateNameMap[dateKeyStr]![name] =
|
||||||
|
dateNameMap[dateKeyStr]![name]! + count;
|
||||||
|
} else {
|
||||||
|
dateNameMap[dateKeyStr]![name] = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetLists = isCompare
|
||||||
|
? {
|
||||||
|
'rama1': chartDataCallsrama1Compare,
|
||||||
|
'shahd': chartDataCallsShahdCompare,
|
||||||
|
'rama2': chartDataCallsRama2Compare,
|
||||||
|
'mayar': chartDataCallsSefer4Compare,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
'rama1': chartDataCallsrama1,
|
||||||
|
'shahd': chartDataCallsShahd,
|
||||||
|
'rama2': chartDataCallsRama2,
|
||||||
|
'mayar': chartDataCallsSefer4,
|
||||||
|
};
|
||||||
|
|
||||||
|
int totalDays = end.difference(start).inDays + 1;
|
||||||
|
|
||||||
|
targetLists.forEach((key, listToFill) {
|
||||||
|
for (int i = 0; i < totalDays; i++) {
|
||||||
|
DateTime currentDate = start.add(Duration(days: i));
|
||||||
|
String currentDateStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||||
|
|
||||||
|
double value = 0;
|
||||||
|
Map<String, double>? dayData = dateNameMap[currentDateStr];
|
||||||
|
|
||||||
|
if (dayData != null) {
|
||||||
|
// if (key == 'mayar_group') {
|
||||||
|
// value = (dayData['mayar'] ?? 0) +
|
||||||
|
// (dayData['rama1'] ?? 0) +
|
||||||
|
// (dayData['sefer4'] ?? 0);
|
||||||
|
// } else {
|
||||||
|
value = dayData[key] ?? 0;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
listToFill.add(FlSpot((i + 1).toDouble(), value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Log.print('Error in fetchEditorCalls: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 🔴 FIXED: Fetch Employment Stats with Unique Check ---
|
||||||
|
Future<void> fetchEmploymentStats() async {
|
||||||
|
try {
|
||||||
|
// لا نستخدم .clear() هنا، سنقوم باستبدال القائمة بالكامل في النهاية
|
||||||
|
|
||||||
|
var res = await CRUD().get(
|
||||||
|
link: AppLink.getEmployeeDriverAfterCallingRegister,
|
||||||
|
payload: _getPayload(startDate!, endDate!));
|
||||||
|
|
||||||
|
if (res == 'failure') return;
|
||||||
|
|
||||||
|
var jsonResponse = jsonDecode(res);
|
||||||
|
if (jsonResponse['status'] == 'success') {
|
||||||
|
if (jsonResponse['message'] != null &&
|
||||||
|
jsonResponse['message']['data'] != null) {
|
||||||
|
List<dynamic> data = jsonResponse['message']['data'];
|
||||||
|
|
||||||
|
List<String> allowedNames = ['shahd', 'mayar', 'rama1', 'rama2'];
|
||||||
|
|
||||||
|
// استخدام Map لضمان عدم تكرار الأسماء (تجميع القيم)
|
||||||
|
Map<String, int> uniqueMap = {};
|
||||||
|
|
||||||
|
for (var item in data) {
|
||||||
|
String name =
|
||||||
|
item['employmentType'].toString().toLowerCase().trim();
|
||||||
|
int count = int.tryParse(item['count'].toString()) ?? 0;
|
||||||
|
|
||||||
|
if (allowedNames.contains(name)) {
|
||||||
|
if (uniqueMap.containsKey(name)) {
|
||||||
|
uniqueMap[name] = uniqueMap[name]! + count;
|
||||||
|
} else {
|
||||||
|
uniqueMap[name] = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تحويل الـ Map إلى القائمة النهائية
|
||||||
|
List<Map<String, dynamic>> tempList = [];
|
||||||
|
uniqueMap.forEach((key, value) {
|
||||||
|
tempList.add({'name': key, 'count': value});
|
||||||
|
});
|
||||||
|
|
||||||
|
// استبدال القائمة القديمة بالقائمة الجديدة النظيفة
|
||||||
|
employmentStatsList = tempList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.print("Error fetchEmploymentStats: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fetch Daily Notes Log ---
|
||||||
|
Future<void> fetchDailyNotes(DateTime date) async {
|
||||||
|
try {
|
||||||
|
isLoadingNotes = true;
|
||||||
|
dailyNotesList.clear();
|
||||||
|
update();
|
||||||
|
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: AppLink.getNotesForEmployee,
|
||||||
|
payload: {"date": DateFormat('yyyy-MM-dd').format(date)});
|
||||||
|
|
||||||
|
if (res != 'failure') {
|
||||||
|
var jsonResponse = (res);
|
||||||
|
if (jsonResponse['status'] == 'success') {
|
||||||
|
dailyNotesList = jsonResponse['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.print("Error fetchDailyNotes: $e");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoadingNotes = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchDrivers() async {
|
|
||||||
isLoading = true;
|
|
||||||
update(); // Notify the observers about the loading state change
|
|
||||||
|
|
||||||
var res = await CRUD().get(
|
|
||||||
link: AppLink.getdriverstotalMonthly,
|
|
||||||
payload: {},
|
|
||||||
);
|
|
||||||
jsonData2 = jsonDecode(res);
|
|
||||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
|
||||||
isLoading = false;
|
|
||||||
final List<dynamic> jsonData = jsonResponse['message'];
|
|
||||||
staticList = jsonData;
|
|
||||||
totalMonthlyDrivers = jsonData[0]['totalDrivers'].toString();
|
|
||||||
driversData = jsonData.map<MonthlyDriverInstall>((item) {
|
|
||||||
return MonthlyDriverInstall.fromJson(item);
|
|
||||||
}).toList();
|
|
||||||
final List<FlSpot> spots = driversData
|
|
||||||
.map((data) => FlSpot(
|
|
||||||
data.day.toDouble(),
|
|
||||||
data.dailyTotalDrivers.toDouble(),
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
chartDataDrivers = spots;
|
|
||||||
final List<FlSpot> spotsCalling = driversData
|
|
||||||
.map((data) => FlSpot(
|
|
||||||
data.day.toDouble(),
|
|
||||||
data.dailyTotalCallingDrivers.toDouble(),
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
chartDataDriversCalling = spotsCalling;
|
|
||||||
final List<FlSpot> spotsTotalMatchingNotes = driversData
|
|
||||||
.map((data) => FlSpot(
|
|
||||||
data.day.toDouble(),
|
|
||||||
data.dailyMatchingNotes.toDouble(),
|
|
||||||
))
|
|
||||||
.toList();
|
|
||||||
chartDataDriversMatchingNotes = spotsTotalMatchingNotes;
|
|
||||||
|
|
||||||
update(); // Notify the observers about the data and loading state change
|
|
||||||
}
|
|
||||||
|
|
||||||
Future getAll() async {
|
|
||||||
await fetch();
|
|
||||||
await fetchRides();
|
|
||||||
await fetchDrivers();
|
|
||||||
await fetchEmployee();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ class OtpHelper extends GetxController {
|
|||||||
/// إرسال OTP
|
/// إرسال OTP
|
||||||
static Future<bool> sendOtp(String phoneNumber) async {
|
static Future<bool> sendOtp(String phoneNumber) async {
|
||||||
try {
|
try {
|
||||||
|
// await CRUD().getJWT();
|
||||||
final response = await CRUD().post(
|
final response = await CRUD().post(
|
||||||
link: _sendOtpUrl,
|
link: _sendOtpUrl,
|
||||||
payload: {'receiver': phoneNumber},
|
payload: {'receiver': phoneNumber},
|
||||||
);
|
);
|
||||||
|
// Log.print('_sendOtpUrl: ${_sendOtpUrl}');
|
||||||
|
// Log.print('response: ${response}');
|
||||||
if (response != 'failure') {
|
if (response != 'failure') {
|
||||||
mySnackeBarError('تم إرسال رمز التحقق إلى رقمك عبر WhatsApp');
|
mySnackbarSuccess('تم إرسال رمز التحقق إلى رقمك عبر WhatsApp');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
mySnackeBarError('حدث خطأ من الخادم. حاول مجددًا.');
|
mySnackeBarError('حدث خطأ من الخادم. حاول مجددًا.');
|
||||||
|
|||||||
37
lib/controller/drivers/driver_not_active_controller.dart
Normal file
37
lib/controller/drivers/driver_not_active_controller.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../constant/links.dart';
|
||||||
|
import '../functions/crud.dart';
|
||||||
|
|
||||||
|
class DriverController extends GetxController {
|
||||||
|
List drivers = [];
|
||||||
|
Map driverDetails = {};
|
||||||
|
|
||||||
|
// جلب السائقين pending
|
||||||
|
getDriversPending() async {
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: AppLink.getDriversPending, // رابط drivers_pending_list.php
|
||||||
|
payload: {},
|
||||||
|
);
|
||||||
|
if (res != 'failure') {
|
||||||
|
drivers = (res)['message'];
|
||||||
|
update(['drivers']); // تحديث الـ UI
|
||||||
|
} else {
|
||||||
|
Get.snackbar('Error', 'Failed to load drivers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// جلب تفاصيل سائق واحد
|
||||||
|
getDriverDetails(String driverId) async {
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: AppLink.getDriverDetails, // رابط driver_details.php
|
||||||
|
payload: {"id": driverId},
|
||||||
|
);
|
||||||
|
if (res != 'failure') {
|
||||||
|
driverDetails = (res)['message'];
|
||||||
|
update(['driverDetails']); // تحديث صفحة التفاصيل
|
||||||
|
} else {
|
||||||
|
Get.snackbar('Error', 'Failed to load driver details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/controller/firebase/notification_service.dart
Normal file
59
lib/controller/firebase/notification_service.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../../print.dart';
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
// تأكد من أن هذا هو الرابط الصحيح لملف الإرسال
|
||||||
|
static const String _serverUrl =
|
||||||
|
'https://syria.intaleq.xyz/intaleq/fcm/send_fcm.php';
|
||||||
|
|
||||||
|
static Future<void> sendNotification({
|
||||||
|
required String target,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
required String? category, // <-- [الإضافة الأولى]
|
||||||
|
String? tone,
|
||||||
|
List<String>? driverList,
|
||||||
|
bool isTopic = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final Map<String, dynamic> payload = {
|
||||||
|
'target': target,
|
||||||
|
'title': title,
|
||||||
|
'body': body,
|
||||||
|
'isTopic': isTopic,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (category != null) {
|
||||||
|
payload['category'] = category; // <-- [الإضافة الثانية]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tone != null) {
|
||||||
|
payload['tone'] = tone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (driverList != null) {
|
||||||
|
// [مهم] تطبيق السائق يرسل passengerList
|
||||||
|
payload['passengerList'] = jsonEncode(driverList);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse(_serverUrl),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
},
|
||||||
|
body: jsonEncode(payload),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
print('✅ Notification sent successfully.');
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
'❌ Failed to send notification. Status code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ An error occurred while sending notification: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||||
@@ -16,9 +14,7 @@ import '../../constant/links.dart';
|
|||||||
import '../../env/env.dart';
|
import '../../env/env.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
import '../../views/widgets/elevated_btn.dart';
|
|
||||||
import 'device_info.dart';
|
import 'device_info.dart';
|
||||||
import 'encrypt_decrypt.dart';
|
|
||||||
import 'security_checks.dart';
|
import 'security_checks.dart';
|
||||||
|
|
||||||
class CRUD {
|
class CRUD {
|
||||||
@@ -225,7 +221,7 @@ class CRUD {
|
|||||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||||
print('fingerPrint: ${fingerPrint}');
|
print('fingerPrint: ${fingerPrint}');
|
||||||
|
|
||||||
await SecurityChecks.isDeviceRootedFromNative(Get.context!);
|
//await SecurityChecks.isDeviceRootedFromNative(Get.context!);
|
||||||
|
|
||||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||||
var payload = {
|
var payload = {
|
||||||
@@ -239,11 +235,11 @@ class CRUD {
|
|||||||
Uri.parse(AppLink.loginWalletAdmin),
|
Uri.parse(AppLink.loginWalletAdmin),
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
);
|
||||||
// Log.print('response.request: ${response1.request}');
|
Log.print('response.request: ${response1.request}');
|
||||||
// Log.print('response.body: ${response1.body}');
|
Log.print('response.body: ${response1.body}');
|
||||||
// print(payload);
|
print(payload);
|
||||||
// Log.print(
|
Log.print(
|
||||||
// 'jsonDecode(response1.body)["jwt"]: ${jsonDecode(response1.body)['jwt']}');
|
'jsonDecode(response1.body)["jwt"]: ${jsonDecode(response1.body)['jwt']}');
|
||||||
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
|
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
|
||||||
return jsonDecode(response1.body)['jwt'].toString();
|
return jsonDecode(response1.body)['jwt'].toString();
|
||||||
}
|
}
|
||||||
@@ -268,9 +264,9 @@ class CRUD {
|
|||||||
'X-HMAC-Auth': hmac.toString(),
|
'X-HMAC-Auth': hmac.toString(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Log.print('response.request:${response.request}');
|
// Log.print('response.request:${response.request}');
|
||||||
Log.print('response.body: ${response.body}');
|
// Log.print('response.body: ${response.body}');
|
||||||
Log.print('payload:$payload');
|
// Log.print('payload:$payload');
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
try {
|
try {
|
||||||
var jsonData = jsonDecode(response.body);
|
var jsonData = jsonDecode(response.body);
|
||||||
|
|||||||
@@ -5,12 +5,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
|
||||||
import '../../main.dart';
|
|
||||||
|
|
||||||
class DeviceHelper {
|
class DeviceHelper {
|
||||||
static Future<String> getDeviceFingerprint() async {
|
static Future<String> getDeviceFingerprint() async {
|
||||||
@@ -27,13 +21,17 @@ class DeviceHelper {
|
|||||||
// Fetch iOS-specific device information
|
// Fetch iOS-specific device information
|
||||||
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
||||||
deviceData = iosInfo.toMap(); // Convert to a map for easier access
|
deviceData = iosInfo.toMap(); // Convert to a map for easier access
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
// Fetch macOS-specific device information
|
||||||
|
MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo;
|
||||||
|
deviceData = macInfo.toMap();
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedError('Unsupported platform');
|
throw UnsupportedError('Unsupported platform');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract relevant device information
|
// Extract relevant device information
|
||||||
final String deviceId = Platform.isAndroid
|
final String deviceId = Platform.isAndroid
|
||||||
? deviceData['androidId'] ?? deviceData['serialNumber'] ?? 'unknown'
|
? deviceData['fingerprint'] ?? 'unknown'
|
||||||
: deviceData['identifierForVendor'] ?? 'unknown';
|
: deviceData['identifierForVendor'] ?? 'unknown';
|
||||||
|
|
||||||
final String deviceModel = deviceData['model'] ?? 'unknown';
|
final String deviceModel = deviceData['model'] ?? 'unknown';
|
||||||
@@ -45,6 +43,7 @@ class DeviceHelper {
|
|||||||
|
|
||||||
// Generate and return the encrypted fingerprint
|
// Generate and return the encrypted fingerprint
|
||||||
final String fingerprint = '${deviceId}_${deviceModel}_$osVersion';
|
final String fingerprint = '${deviceId}_${deviceModel}_$osVersion';
|
||||||
|
|
||||||
// print(EncryptionHelper.instance.encryptData(fingerprint));
|
// print(EncryptionHelper.instance.encryptData(fingerprint));
|
||||||
return (fingerprint);
|
return (fingerprint);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ class EncryptionHelper {
|
|||||||
}
|
}
|
||||||
debugPrint("Initializing EncryptionHelper...");
|
debugPrint("Initializing EncryptionHelper...");
|
||||||
var keyOfApp = r(Env.keyOfApp).toString().split(Env.addd)[0];
|
var keyOfApp = r(Env.keyOfApp).toString().split(Env.addd)[0];
|
||||||
Log.print('keyOfApp: ${keyOfApp}');
|
// Log.print('keyOfApp: ${keyOfApp}');
|
||||||
var initializationVector =
|
var initializationVector =
|
||||||
r(Env.initializationVector).toString().split(Env.addd)[0];
|
r(Env.initializationVector).toString().split(Env.addd)[0];
|
||||||
Log.print('initializationVector: ${initializationVector}');
|
// Log.print('initializationVector: ${initializationVector}');
|
||||||
|
|
||||||
// Set the global instance
|
// Set the global instance
|
||||||
_instance = EncryptionHelper._(
|
_instance = EncryptionHelper._(
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import 'package:path_provider/path_provider.dart' as path_provider;
|
|||||||
import '../../constant/api_key.dart';
|
import '../../constant/api_key.dart';
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../constant/colors.dart';
|
import '../../constant/colors.dart';
|
||||||
|
import '../../constant/info.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
|
import 'encrypt_decrypt.dart';
|
||||||
|
|
||||||
class ImageController extends GetxController {
|
class ImageController extends GetxController {
|
||||||
File? myImage;
|
File? myImage;
|
||||||
@@ -152,7 +154,7 @@ class ImageController extends GetxController {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error in choosImage: $e');
|
print('Error in choosImage: $e');
|
||||||
Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||||
backgroundColor: AppColor.primaryColor);
|
backgroundColor: AppColor.redColor);
|
||||||
} finally {
|
} finally {
|
||||||
isloading = false;
|
isloading = false;
|
||||||
update();
|
update();
|
||||||
@@ -241,20 +243,25 @@ class ImageController extends GetxController {
|
|||||||
'POST',
|
'POST',
|
||||||
Uri.parse(link),
|
Uri.parse(link),
|
||||||
);
|
);
|
||||||
|
Log.print('request: ${request}');
|
||||||
var length = await file.length();
|
var length = await file.length();
|
||||||
var stream = http.ByteStream(file.openRead());
|
var stream = http.ByteStream(file.openRead());
|
||||||
|
final headers = {
|
||||||
|
'Authorization':
|
||||||
|
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||||||
|
// 'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||||
|
};
|
||||||
|
Log.print('headers: ${headers}');
|
||||||
|
|
||||||
var multipartFile = http.MultipartFile(
|
var multipartFile = http.MultipartFile(
|
||||||
'image',
|
'image',
|
||||||
stream,
|
stream,
|
||||||
length,
|
length,
|
||||||
filename: basename(file.path),
|
filename: basename(file.path),
|
||||||
);
|
);
|
||||||
request.headers.addAll({
|
request.headers.addAll(headers);
|
||||||
'Authorization':
|
|
||||||
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials.toString()))}',
|
|
||||||
});
|
|
||||||
// Set the file name to the driverID
|
// Set the file name to the driverID
|
||||||
|
|
||||||
request.files.add(
|
request.files.add(
|
||||||
http.MultipartFile(
|
http.MultipartFile(
|
||||||
'image',
|
'image',
|
||||||
@@ -270,8 +277,10 @@ class ImageController extends GetxController {
|
|||||||
var res = await http.Response.fromStream(myrequest);
|
var res = await http.Response.fromStream(myrequest);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
Log.print('jsonDecode(res.body): ${jsonDecode(res.body)}');
|
Log.print('jsonDecode(res.body): ${jsonDecode(res.body)}');
|
||||||
|
if (jsonDecode(res.body)['status'] == 'Image uploaded successfully!') {
|
||||||
Get.snackbar('title', 'message', backgroundColor: AppColor.greenColor);
|
Get.snackbar('Success'.tr, 'Image uploaded successfully!'.tr,
|
||||||
|
backgroundColor: AppColor.greenColor);
|
||||||
|
}
|
||||||
return jsonDecode(res.body);
|
return jsonDecode(res.body);
|
||||||
} else {
|
} else {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
|
|||||||
@@ -43,6 +43,38 @@ class WalletController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future addDriverWallet(String paymentMethod, driverID, point, phone) async {
|
||||||
|
// paymentToken = await generateToken(count);
|
||||||
|
// var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||||
|
await CRUD().postWallet(link: AppLink.addFromAdmin, payload: {
|
||||||
|
'driverID': driverID.toString(),
|
||||||
|
'paymentID': 'gift_connect_$driverID${DateTime.timestamp()}'.toString(),
|
||||||
|
'amount': point,
|
||||||
|
'token': 'gift_connect',
|
||||||
|
'paymentMethod': paymentMethod,
|
||||||
|
'phone': phone,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future addDrivergift3000(String paymentMethod, driverID, point, phone) async {
|
||||||
|
// paymentToken = await generateToken(count);
|
||||||
|
// var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||||
|
var res = await CRUD().postWallet(link: AppLink.add300ToDriver, payload: {
|
||||||
|
'driverID': driverID.toString(),
|
||||||
|
'paymentID': paymentMethod,
|
||||||
|
'amount': point,
|
||||||
|
'token': 'gift_connect_30000',
|
||||||
|
'paymentMethod': paymentMethod,
|
||||||
|
'phone': phone,
|
||||||
|
});
|
||||||
|
if (res != 'failure') {
|
||||||
|
Get.snackbar('success', 'addDrivergift3000',
|
||||||
|
backgroundColor: AppColor.greenColor);
|
||||||
|
} else {
|
||||||
|
Get.snackbar('error', res, backgroundColor: AppColor.redColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future addSeferWallet(String point, driverID) async {
|
Future addSeferWallet(String point, driverID) async {
|
||||||
var amount = (int.parse(point) * -1).toStringAsFixed(0);
|
var amount = (int.parse(point) * -1).toStringAsFixed(0);
|
||||||
var seferToken = await generateTokenDriver(amount, driverID);
|
var seferToken = await generateTokenDriver(amount, driverID);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:sefer_admin1/views/widgets/my_textField.dart';
|
|||||||
|
|
||||||
import '../constant/style.dart';
|
import '../constant/style.dart';
|
||||||
import '../print.dart';
|
import '../print.dart';
|
||||||
|
import 'firebase/notification_service.dart';
|
||||||
|
|
||||||
class NotificationController extends GetxController {
|
class NotificationController extends GetxController {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
@@ -18,14 +19,14 @@ class NotificationController extends GetxController {
|
|||||||
List<String> tokensDriver = [];
|
List<String> tokensDriver = [];
|
||||||
List<String> tokensPassengers = [];
|
List<String> tokensPassengers = [];
|
||||||
|
|
||||||
getTokensDrivers() async {
|
// getTokensDrivers() async {
|
||||||
await FirebaseMessagesController().loadAllPagesAndSendNotifications();
|
// await FirebaseMessagesController().loadAllPagesAndSendNotifications();
|
||||||
}
|
// }
|
||||||
|
|
||||||
getTokensPassengers() async {
|
// getTokensPassengers() async {
|
||||||
await FirebaseMessagesController()
|
// await FirebaseMessagesController()
|
||||||
.loadAllPagesAndSendNotificationsPassengers();
|
// .loadAllPagesAndSendNotificationsPassengers();
|
||||||
}
|
// }
|
||||||
|
|
||||||
Future<dynamic> sendNotificationDrivers() {
|
Future<dynamic> sendNotificationDrivers() {
|
||||||
return Get.defaultDialog(
|
return Get.defaultDialog(
|
||||||
@@ -60,34 +61,40 @@ class NotificationController extends GetxController {
|
|||||||
// tokensDriver = box.read(BoxName.tokensDrivers)['message'];
|
// tokensDriver = box.read(BoxName.tokensDrivers)['message'];
|
||||||
// Log.print('tokensDriver: ${tokensDriver}');
|
// Log.print('tokensDriver: ${tokensDriver}');
|
||||||
// if (formKey.currentState!.validate()) {
|
// if (formKey.currentState!.validate()) {
|
||||||
box.read(BoxName.tokensDrivers)['message'].length;
|
// box.read(BoxName.tokensDrivers)['message'].length;
|
||||||
for (var i = 0;
|
// for (var i = 0;
|
||||||
i < box.read(BoxName.tokensDrivers)['message'].length;
|
// i < box.read(BoxName.tokensDrivers)['message'].length;
|
||||||
i++) {
|
// i++) {
|
||||||
// for (var i = 0; i < 2; i++) {
|
// // for (var i = 0; i < 2; i++) {
|
||||||
// print(i);
|
// // print(i);
|
||||||
var res = await CRUD()
|
// var res = await CRUD()
|
||||||
.post(link: AppLink.addNotificationCaptain, payload: {
|
// .post(link: AppLink.addNotificationCaptain, payload: {
|
||||||
"driverID": box
|
// "driverID": box
|
||||||
.read(BoxName.tokensDrivers)['message'][i]['id']
|
// .read(BoxName.tokensDrivers)['message'][i]['id']
|
||||||
.toString(),
|
// .toString(),
|
||||||
"title": title.text,
|
// "title": title.text,
|
||||||
"body": body.text,
|
// "body": body.text,
|
||||||
"isPin": 'unPin',
|
// "isPin": 'unPin',
|
||||||
});
|
// });
|
||||||
Log.print(
|
// Log.print(
|
||||||
'res: ${res}for ${box.read(BoxName.tokensDrivers)['message'][i]['id']}');
|
// 'res: ${res}for ${box.read(BoxName.tokensDrivers)['message'][i]['id']}');
|
||||||
// Log.print('tokensDriver[i]: ${tokensDriver[i]}');
|
// // Log.print('tokensDriver[i]: ${tokensDriver[i]}');
|
||||||
Future.delayed(const Duration(microseconds: 50));
|
// Future.delayed(const Duration(microseconds: 50));
|
||||||
|
NotificationService.sendNotification(
|
||||||
FirebaseMessagesController().sendNotificationToAnyWithoutData(
|
target: 'drivers', // الإرسال لجميع المشتركين في "service"
|
||||||
title.text,
|
title: title.text,
|
||||||
body.text,
|
body: body.text,
|
||||||
box
|
isTopic: true,
|
||||||
.read(BoxName.tokensDrivers)['message'][i]['token']
|
category: 'fromAdmin', // فئة توضح نوع الإشعار
|
||||||
.toString(),
|
);
|
||||||
'tone2.wav');
|
// FirebaseMessagesController().sendNotificationToAnyWithoutData(
|
||||||
}
|
// title.text,
|
||||||
|
// body.text,
|
||||||
|
// box
|
||||||
|
// .read(BoxName.tokensDrivers)['message'][i]['token']
|
||||||
|
// .toString(),
|
||||||
|
// 'tone2.wav');
|
||||||
|
// }
|
||||||
Get.back();
|
Get.back();
|
||||||
// }
|
// }
|
||||||
}),
|
}),
|
||||||
@@ -129,38 +136,45 @@ class NotificationController extends GetxController {
|
|||||||
title: 'send'.tr,
|
title: 'send'.tr,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// tokensPassengers = box.read(BoxName.tokensPassengers);
|
// tokensPassengers = box.read(BoxName.tokensPassengers);
|
||||||
var tokensPassengersData =
|
// var tokensPassengersData =
|
||||||
box.read(BoxName.tokensPassengers)['data'];
|
// box.read(BoxName.tokensPassengers)['data'];
|
||||||
|
|
||||||
// Debug print to check structure of the 'data' field
|
// // Debug print to check structure of the 'data' field
|
||||||
print('Tokens Passengers Data: $tokensPassengersData');
|
// print('Tokens Passengers Data: $tokensPassengersData');
|
||||||
|
|
||||||
if (tokensPassengersData is List) {
|
// if (tokensPassengersData is List) {
|
||||||
for (var i = 0; i < tokensPassengersData.length; i++) {
|
// for (var i = 0; i < tokensPassengersData.length; i++) {
|
||||||
if (formKey.currentState!.validate()) {
|
// if (formKey.currentState!.validate()) {
|
||||||
var res = await CRUD()
|
// var res = await CRUD()
|
||||||
.post(link: AppLink.addNotificationPassenger, payload: {
|
// .post(link: AppLink.addNotificationPassenger, payload: {
|
||||||
"passenger_id":
|
// "passenger_id":
|
||||||
tokensPassengersData[i]['passengerID'].toString(),
|
// tokensPassengersData[i]['passengerID'].toString(),
|
||||||
"title": title.text,
|
// "title": title.text,
|
||||||
"body": body.text,
|
// "body": body.text,
|
||||||
});
|
// });
|
||||||
Log.print('res: ${res}');
|
// Log.print('res: ${res}');
|
||||||
FirebaseMessagesController()
|
// FirebaseMessagesController()
|
||||||
.sendNotificationToAnyWithoutData(
|
// .sendNotificationToAnyWithoutData(
|
||||||
title.text,
|
// title.text,
|
||||||
body.text,
|
// body.text,
|
||||||
tokensPassengersData[i]['token']
|
// tokensPassengersData[i]['token']
|
||||||
.toString(), // Access token correctly
|
// .toString(), // Access token correctly
|
||||||
'order.wav',
|
// 'order.wav',
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
NotificationService.sendNotification(
|
||||||
|
target: 'passengers', // الإرسال لجميع المشتركين في "service"
|
||||||
|
title: title.text,
|
||||||
|
body: body.text,
|
||||||
|
isTopic: true,
|
||||||
|
category: 'fromAdmin', // فئة توضح نوع الإشعار
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
Get.back();
|
Get.back();
|
||||||
} else {
|
// } else {
|
||||||
// Handle the case where 'data' is not a list
|
// // Handle the case where 'data' is not a list
|
||||||
print('Data is not a list: $tokensPassengersData');
|
// print('Data is not a list: $tokensPassengersData');
|
||||||
}
|
// }
|
||||||
}),
|
}),
|
||||||
cancel: MyElevatedButton(
|
cancel: MyElevatedButton(
|
||||||
title: 'cancel',
|
title: 'cancel',
|
||||||
|
|||||||
139
lib/controller/rides/ride_lookup_controller.dart
Normal file
139
lib/controller/rides/ride_lookup_controller.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../constant/links.dart';
|
||||||
|
import '../functions/crud.dart';
|
||||||
|
|
||||||
|
class RideLookupController extends GetxController {
|
||||||
|
final TextEditingController phoneCtrl = TextEditingController();
|
||||||
|
|
||||||
|
bool isLoading = false;
|
||||||
|
Map<String, dynamic>? passenger; // {id, first_name, last_name, phone}
|
||||||
|
Map<String, dynamic>? ride; // Ride details
|
||||||
|
|
||||||
|
// Status filter for the search tab
|
||||||
|
String currentStatusFilter = '';
|
||||||
|
|
||||||
|
// Whitelist of allowed statuses for the Update Dropdown
|
||||||
|
// UPDATED: Matches the exact types you requested
|
||||||
|
final List<String> statusOptions = const [
|
||||||
|
'Pending',
|
||||||
|
'Accepted',
|
||||||
|
'EnRoute',
|
||||||
|
'Arrived',
|
||||||
|
'Started',
|
||||||
|
'Completed',
|
||||||
|
'Canceled',
|
||||||
|
];
|
||||||
|
|
||||||
|
String? selectedStatus;
|
||||||
|
|
||||||
|
// Hydrate dropdown value from the current ride data
|
||||||
|
void hydrateSelectedFromRide() {
|
||||||
|
final cur = (ride?['status'] ?? '') as String;
|
||||||
|
selectedStatus = statusOptions.contains(cur) ? cur : null;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updateRideStatus({String? note}) async {
|
||||||
|
if (ride == null) return false;
|
||||||
|
if (selectedStatus == null || selectedStatus!.isEmpty) return false;
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await CRUD().post(
|
||||||
|
link: AppLink.admin_update_ride_status,
|
||||||
|
payload: {
|
||||||
|
'id': "${ride!['id']}",
|
||||||
|
'status': selectedStatus!,
|
||||||
|
if (note != null && note.trim().isNotEmpty) 'reason': note.trim(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final d = jsonDecode(res);
|
||||||
|
final ok = (d['status'] == 'success');
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
// Update local ride details from response
|
||||||
|
final updated = (d['message'] ?? d)['ride'];
|
||||||
|
if (updated != null) {
|
||||||
|
ride = Map<String, dynamic>.from(updated);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated to accept status filter
|
||||||
|
Future<bool> searchLatest({String? status}) async {
|
||||||
|
final phone = phoneCtrl.text.trim();
|
||||||
|
|
||||||
|
// If status is passed, update the current filter
|
||||||
|
if (status != null) {
|
||||||
|
currentStatusFilter = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If phone is empty, we stop unless your API supports fetching "latest of all users"
|
||||||
|
if (phone.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
update();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final res = await CRUD().post(
|
||||||
|
link: AppLink.admin_get_rides_by_phone,
|
||||||
|
payload: {
|
||||||
|
'phone': phone,
|
||||||
|
// If filter is 'All', send empty string to PHP, otherwise send the exact status
|
||||||
|
'status': currentStatusFilter == 'All' ? '' : currentStatusFilter,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final d = res;
|
||||||
|
|
||||||
|
if (d['status'] == 'success') {
|
||||||
|
passenger = (d['message'] ?? d)['passenger'];
|
||||||
|
ride = (d['message'] ?? d)['ride'];
|
||||||
|
|
||||||
|
// Hydrate the dropdown for the update section based on the fetched ride
|
||||||
|
hydrateSelectedFromRide();
|
||||||
|
|
||||||
|
update();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
passenger = null;
|
||||||
|
ride = null;
|
||||||
|
update();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
passenger = null;
|
||||||
|
ride = null;
|
||||||
|
update();
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String rideHeader() {
|
||||||
|
if (ride == null) return '';
|
||||||
|
final id = ride!['id'] ?? '';
|
||||||
|
final st = (ride!['status'] ?? '—').toString();
|
||||||
|
return "Ride #$id — $st";
|
||||||
|
}
|
||||||
|
}
|
||||||
25358
lib/env/env.g.dart
vendored
25358
lib/env/env.g.dart
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -9,12 +8,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:sefer_admin1/views/auth/login_page.dart';
|
import 'package:sefer_admin1/views/auth/login_page.dart';
|
||||||
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'controller/firebase/firbase_messge.dart';
|
import 'controller/firebase/firbase_messge.dart';
|
||||||
import 'controller/functions/encrypt_decrypt.dart';
|
import 'controller/functions/encrypt_decrypt.dart';
|
||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
import 'models/db_sql.dart';
|
import 'models/db_sql.dart';
|
||||||
import 'views/admin/admin_home_page.dart';
|
|
||||||
|
|
||||||
final box = GetStorage();
|
final box = GetStorage();
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
@@ -31,12 +29,13 @@ DbSql sql = DbSql.instance;
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await GetStorage.init();
|
await GetStorage.init();
|
||||||
|
await initializeDateFormatting('ar', null);
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
if (Platform.isAndroid || Platform.isIOS) {
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
);
|
);
|
||||||
await FirebaseMessagesController().requestFirebaseMessagingPermission();
|
// await FirebaseMessagesController().requestFirebaseMessagingPermission();
|
||||||
|
|
||||||
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ void main() async {
|
|||||||
DeviceOrientation.portraitDown,
|
DeviceOrientation.portraitDown,
|
||||||
]);
|
]);
|
||||||
} // Enable Crashlytics collection
|
} // Enable Crashlytics collection
|
||||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
|
// FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
|
||||||
|
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,174 +1,231 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_admin1/constant/colors.dart';
|
import 'package:sefer_admin1/constant/colors.dart';
|
||||||
import 'package:sefer_admin1/controller/admin/dashboard_controller.dart';
|
import 'package:sefer_admin1/controller/admin/dashboard_controller.dart';
|
||||||
import 'package:sefer_admin1/controller/admin/register_captain_controller.dart';
|
|
||||||
import 'package:sefer_admin1/controller/admin/static_controller.dart';
|
import 'package:sefer_admin1/controller/admin/static_controller.dart';
|
||||||
import 'package:sefer_admin1/controller/notification_controller.dart';
|
import 'package:sefer_admin1/controller/notification_controller.dart';
|
||||||
import 'package:sefer_admin1/main.dart';
|
import 'package:sefer_admin1/main.dart';
|
||||||
import 'package:sefer_admin1/views/admin/captain/drivers_cant_registe.dart';
|
import 'package:sefer_admin1/views/admin/drivers/driver_tracker_screen.dart';
|
||||||
|
import 'package:sefer_admin1/views/admin/error/error/error_page.dart';
|
||||||
import 'package:sefer_admin1/views/widgets/mycircular.dart';
|
import 'package:sefer_admin1/views/widgets/mycircular.dart';
|
||||||
|
|
||||||
// Please make sure all these imports are correct for your project structure
|
// تأكد من صحة المسارات
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../constant/links.dart';
|
|
||||||
import '../../constant/style.dart';
|
|
||||||
import '../../controller/functions/crud.dart';
|
import '../../controller/functions/crud.dart';
|
||||||
import '../invoice/invoice_list_page.dart';
|
import '../invoice/invoice_list_page.dart';
|
||||||
import '../widgets/my_scafold.dart';
|
|
||||||
import '../widgets/my_textField.dart';
|
import '../widgets/my_textField.dart';
|
||||||
import '../invoice/add_invoice_page.dart';
|
|
||||||
import 'captain/captain.dart';
|
import 'captain/captain.dart';
|
||||||
import 'dashboard_widget.dart'; // Assuming DashboardStatCard is here
|
import 'captain/syrian_driver_not_active.dart';
|
||||||
|
import 'drivers/driver_gift_check_page.dart';
|
||||||
import 'drivers/driver_the_best.dart';
|
import 'drivers/driver_the_best.dart';
|
||||||
|
import 'drivers/monitor_ride.dart';
|
||||||
import 'employee/employee_page.dart';
|
import 'employee/employee_page.dart';
|
||||||
import 'packages.dart';
|
import 'packages.dart';
|
||||||
import 'passenger/passenger.dart';
|
import 'passenger/passenger.dart';
|
||||||
import 'rides/rides.dart';
|
import 'rides/ride_lookup_page.dart';
|
||||||
import 'static/static.dart';
|
import 'static/static.dart';
|
||||||
import 'wallet/wallet.dart';
|
import 'wallet/wallet.dart';
|
||||||
|
|
||||||
class AdminHomePage extends StatelessWidget {
|
class AdminHomePage extends StatelessWidget {
|
||||||
AdminHomePage({super.key});
|
AdminHomePage({super.key});
|
||||||
|
|
||||||
// Responsive grid column calculation
|
|
||||||
int _calculateCrossAxisCount(BuildContext context) {
|
|
||||||
double screenWidth = MediaQuery.of(context).size.width;
|
|
||||||
if (screenWidth > 1200) return 5; // Large desktops
|
|
||||||
if (screenWidth > 900) return 4; // Desktops
|
|
||||||
if (screenWidth > 600) return 3; // Tablets
|
|
||||||
return 2; // Phones
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to format currency
|
|
||||||
String _formatCurrency(dynamic value) {
|
|
||||||
if (value == null) return '\$0.00';
|
|
||||||
final number = double.tryParse(value.toString());
|
|
||||||
if (number != null) return '\$${number.toStringAsFixed(2)}';
|
|
||||||
return '\$0.00';
|
|
||||||
}
|
|
||||||
|
|
||||||
final TextEditingController _messageController = TextEditingController();
|
final TextEditingController _messageController = TextEditingController();
|
||||||
|
|
||||||
|
// حساب عدد الأعمدة
|
||||||
|
int _calculateCrossAxisCount(BuildContext context, {bool isSmall = false}) {
|
||||||
|
double screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
if (screenWidth > 1200) return isSmall ? 6 : 5;
|
||||||
|
if (screenWidth > 900) return isSmall ? 5 : 4;
|
||||||
|
if (screenWidth > 600) return isSmall ? 4 : 3;
|
||||||
|
return isSmall ? 3 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatCurrency(dynamic value) {
|
||||||
|
if (value == null) return '0.00';
|
||||||
|
final number = double.tryParse(value.toString());
|
||||||
|
if (number != null) return number.toStringAsFixed(2);
|
||||||
|
return '0.00';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Make sure DashboardController is initialized
|
|
||||||
final DashboardController dashboardController =
|
final DashboardController dashboardController =
|
||||||
Get.put(DashboardController());
|
Get.put(DashboardController());
|
||||||
|
|
||||||
// Action items list with Arabic titles
|
// 1. تحديد هوية المستخدم الحالي
|
||||||
|
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||||
|
|
||||||
|
// 2. تحديد من هو "السوبر أدمن" الذي يرى كل شيء
|
||||||
|
// يمكنك إضافة المزيد من الأرقام هنا باستخدام || أو قائمة
|
||||||
|
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||||
|
|
||||||
|
// 3. بناء القائمة باستخدام (Collection If)
|
||||||
final List<Map<String, dynamic>> actionItems = [
|
final List<Map<String, dynamic>> actionItems = [
|
||||||
|
// --- عناصر يراها الجميع ---
|
||||||
{
|
{
|
||||||
'title': 'الركاب',
|
'title': 'الركاب',
|
||||||
'icon': Icons.people_alt_outlined,
|
'icon': Icons.people_alt_rounded,
|
||||||
'onPressed': () => Get.to(() => Passengrs(),
|
'color': Colors.blueAccent,
|
||||||
transition: Transition.rightToLeftWithFade)
|
'onPressed': () =>
|
||||||
|
Get.to(() => Passengrs(), transition: Transition.fadeIn)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'الكباتن',
|
'title': 'الكباتن',
|
||||||
'icon': Icons.sports_motorsports_outlined,
|
'icon': Icons.sports_motorsports_rounded,
|
||||||
|
'color': Colors.orangeAccent,
|
||||||
'onPressed': () =>
|
'onPressed': () =>
|
||||||
Get.to(() => Captain(), transition: Transition.rightToLeftWithFade)
|
Get.to(() => CaptainsPage(), transition: Transition.fadeIn)
|
||||||
},
|
|
||||||
{
|
|
||||||
'title': 'المحفظة',
|
|
||||||
'icon': Icons.account_balance_wallet_outlined,
|
|
||||||
'onPressed': () =>
|
|
||||||
Get.to(() => Wallet(), transition: Transition.rightToLeftWithFade)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'الرحلات',
|
'title': 'الرحلات',
|
||||||
'icon': Icons.directions_car_filled_outlined,
|
'icon': Icons.directions_car_filled_rounded,
|
||||||
|
'color': Colors.indigoAccent,
|
||||||
'onPressed': () =>
|
'onPressed': () =>
|
||||||
Get.to(() => Rides(), transition: Transition.rightToLeftWithFade)
|
Get.to(() => RidesDashboardScreen(), transition: Transition.fadeIn)
|
||||||
|
// Get.to(() => RideLookupPage(), transition: Transition.fadeIn)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': ' الكباتن النشطين',
|
||||||
|
'icon': Icons.directions_car_filled_rounded,
|
||||||
|
'color': Colors.indigoAccent,
|
||||||
|
'onPressed': () =>
|
||||||
|
Get.to(() => IntaleqTrackerScreen(), transition: Transition.fadeIn)
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- عناصر خاصة بالسوبر أدمن فقط (Using Collection If) ---
|
||||||
|
|
||||||
|
if (isSuperAdmin)
|
||||||
|
{
|
||||||
|
'title': 'مراقبة الرحلات'.tr,
|
||||||
|
'icon': Icons.route, // أيقونة مناسبة لمراقبة الرحلات
|
||||||
|
'color': Colors.purpleAccent,
|
||||||
|
'onPressed': () =>
|
||||||
|
Get.to(() => RideMonitorScreen(), transition: Transition.fadeIn),
|
||||||
|
},
|
||||||
|
|
||||||
|
if (isSuperAdmin)
|
||||||
|
{
|
||||||
|
'title': 'المحفظة', // الأمور المالية حساسة
|
||||||
|
'icon': Icons.account_balance_wallet_rounded,
|
||||||
|
'color': Colors.purpleAccent,
|
||||||
|
'onPressed': () =>
|
||||||
|
Get.to(() => Wallet(), transition: Transition.fadeIn)
|
||||||
|
},
|
||||||
|
if (isSuperAdmin)
|
||||||
|
{
|
||||||
|
'title': 'دفع هدية 300'.tr,
|
||||||
|
'icon': Icons.card_giftcard, // أيقونة الهدية
|
||||||
|
'color': Colors.purpleAccent,
|
||||||
|
'onPressed': () => Get.to(() => DriverGiftCheckPage(),
|
||||||
|
transition: Transition.fadeIn),
|
||||||
|
},
|
||||||
|
|
||||||
|
// if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'الإحصائيات',
|
'title': 'الإحصائيات',
|
||||||
'icon': Icons.bar_chart_outlined,
|
'icon': Icons.bar_chart_rounded,
|
||||||
|
'color': Colors.teal,
|
||||||
'onPressed': () async {
|
'onPressed': () async {
|
||||||
await Get.put(StaticController()).getAll();
|
await Get.put(StaticController()).getAll();
|
||||||
Get.to(() => const StaticDash());
|
Get.to(() => const StaticDash());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// هذا هو الجزء الذي طلبته تحديداً
|
||||||
|
if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'إرسال واتساب للسائقين',
|
'title': 'واتساب سائقين',
|
||||||
'icon': Icons.message_outlined,
|
'icon': Icons.message_rounded,
|
||||||
'iconColor': Colors.green.shade600,
|
'color': Colors.green,
|
||||||
'onPressed': () => _showWhatsAppDialog(context)
|
'onPressed': () => _showWhatsAppDialog(context)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- عودة للعناصر العامة (أو يمكنك تقييدها أيضاً) ---
|
||||||
|
|
||||||
|
if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'إرسال إشعار للسائقين',
|
'title': 'إشعار للسائقين',
|
||||||
'icon': Icons.notifications_active_outlined,
|
'icon': Icons.notifications_active_rounded,
|
||||||
|
'color': Colors.deepOrange,
|
||||||
'onPressed': () async =>
|
'onPressed': () async =>
|
||||||
await Get.put(NotificationController()).getTokensDrivers()
|
await Get.put(NotificationController()).sendNotificationDrivers()
|
||||||
},
|
},
|
||||||
|
if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'إرسال إشعار للركاب',
|
'title': 'إشعار للركاب',
|
||||||
'icon': Icons.notification_important_outlined,
|
'icon': Icons.notification_important_rounded,
|
||||||
'onPressed': () async =>
|
'color': Colors.pinkAccent,
|
||||||
await Get.put(NotificationController()).getTokensPassengers()
|
'onPressed': () async => await Get.put(NotificationController())
|
||||||
|
.sendNotificationPassengers()
|
||||||
},
|
},
|
||||||
|
if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'تسجيل كابتن جديد',
|
'title': 'تسجيل كابتن',
|
||||||
'icon': Icons.person_add_alt_1_outlined,
|
'icon': Icons.person_add_alt_1_rounded,
|
||||||
'onPressed': () async {
|
'color': Colors.cyan,
|
||||||
await Get.put(RegisterCaptainController())
|
'onPressed': () => Get.to(() => DriversPendingPage())
|
||||||
.getDriverNotCompleteRegistration();
|
|
||||||
Get.to(() => const DriversCantRegister());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
if (isSuperAdmin) // تحديث الباقات للمدير فقط
|
||||||
{
|
{
|
||||||
'title': 'تحديث الباقات',
|
'title': 'تحديث الباقات',
|
||||||
'icon': Icons.inventory_2_outlined,
|
'icon': Icons.inventory_2_rounded,
|
||||||
|
'color': Colors.brown,
|
||||||
'onPressed': () => Get.to(() => PackageUpdateScreen())
|
'onPressed': () => Get.to(() => PackageUpdateScreen())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
if (isSuperAdmin) // الموظفون للمدير فقط
|
||||||
{
|
{
|
||||||
'title': 'الموظفون',
|
'title': 'الموظفون',
|
||||||
'icon': Icons.badge_outlined,
|
'icon': Icons.badge_rounded,
|
||||||
|
'color': Colors.blueGrey,
|
||||||
'onPressed': () => Get.to(() => EmployeePage())
|
'onPressed': () => Get.to(() => EmployeePage())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'أفضل السائقين',
|
'title': 'أفضل السائقين',
|
||||||
'icon': Icons.star_border_purple500_outlined,
|
'icon': Icons.star_rounded,
|
||||||
'onPressed': () => Get.to(() => DriverTheBest())
|
'color': Colors.amber,
|
||||||
|
'onPressed': () => Get.to(() => DriverTheBestRedesigned())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
if (isSuperAdmin) // الفواتير للمدير فقط
|
||||||
{
|
{
|
||||||
'title': 'إضافة فاتورة',
|
'title': 'الفواتير',
|
||||||
'icon': Icons.post_add_outlined,
|
'icon': Icons.receipt_long_rounded,
|
||||||
// 'onPressed': () => Get.to(() => AddInvoicePage())
|
'color': Colors.green.shade800,
|
||||||
'onPressed': () => Get.to(() => InvoiceListPage())
|
'onPressed': () => Get.to(() => InvoiceListPage())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'إضافة جهاز كمسؤول',
|
'title': 'سجل الأخطاء',
|
||||||
'icon': Icons.admin_panel_settings_outlined,
|
'icon': Icons.error_outline_rounded,
|
||||||
'onPressed': () async => await CRUD()
|
'color': Colors.redAccent,
|
||||||
.post(link: AppLink.addAdminUser, payload: {'name': 'b'})
|
'onPressed': () => Get.to(() => ErrorListPage())
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return MyScafolld(
|
return Scaffold(
|
||||||
title: 'لوحة التحكم الرئيسية',
|
backgroundColor: const Color(0xFFF5F7FA),
|
||||||
action: IconButton(
|
body: RefreshIndicator(
|
||||||
onPressed: () async {
|
onRefresh: () async {
|
||||||
await dashboardController.getDashBoard();
|
await dashboardController.getDashBoard();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.refresh, color: AppColor.primaryColor, size: 28),
|
child: GetBuilder<DashboardController>(builder: (controller) {
|
||||||
tooltip: 'تحديث',
|
|
||||||
),
|
|
||||||
body: [
|
|
||||||
GetBuilder<DashboardController>(builder: (controller) {
|
|
||||||
if (controller.dashbord.isEmpty) {
|
if (controller.dashbord.isEmpty) {
|
||||||
return const MyCircularProgressIndicator();
|
return const Center(child: MyCircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main data map for easier access
|
|
||||||
final data = controller.dashbord[0];
|
final data = controller.dashbord[0];
|
||||||
|
|
||||||
// Stat cards list with Arabic titles
|
// إحصائيات لوحة التحكم
|
||||||
final List<Map<String, dynamic>> statCards = [
|
final List<Map<String, dynamic>> statCards = [
|
||||||
|
// يمكنك تطبيق نفس المنطق هنا لإخفاء الأرباح عن الموظفين العاديين
|
||||||
|
if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'رصيد الرسائل',
|
'title': 'رصيد الرسائل',
|
||||||
'value': controller.creditSMS.toString(),
|
'value': controller.creditSMS.toString(),
|
||||||
@@ -193,12 +250,15 @@ class AdminHomePage extends StatelessWidget {
|
|||||||
'icon': Icons.calendar_month_outlined,
|
'icon': Icons.calendar_month_outlined,
|
||||||
'color': Colors.purple
|
'color': Colors.purple
|
||||||
},
|
},
|
||||||
|
|
||||||
|
if (isSuperAdmin) // إخفاء الأمور المالية
|
||||||
{
|
{
|
||||||
'title': 'متوسط التكلفة',
|
'title': 'متوسط التكلفة',
|
||||||
'value': _formatCurrency(data['avg_passenger_price']),
|
'value': _formatCurrency(data['avg_passenger_price']),
|
||||||
'icon': Icons.monetization_on_outlined,
|
'icon': Icons.monetization_on_outlined,
|
||||||
'color': Colors.green
|
'color': Colors.green
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
'title': 'الرحلات المكتملة',
|
'title': 'الرحلات المكتملة',
|
||||||
'value': data['completed_rides'].toString(),
|
'value': data['completed_rides'].toString(),
|
||||||
@@ -211,24 +271,29 @@ class AdminHomePage extends StatelessWidget {
|
|||||||
'icon': Icons.cancel_outlined,
|
'icon': Icons.cancel_outlined,
|
||||||
'color': AppColor.redColor
|
'color': AppColor.redColor
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// if (isSuperAdmin) // إخفاء المدفوعات
|
||||||
{
|
{
|
||||||
'title': 'مدفوعات السائقين',
|
'title': 'مدفوعات السائقين',
|
||||||
'value': _formatCurrency(data['payments']),
|
'value': _formatCurrency(data['payments']),
|
||||||
'icon': Icons.payments_outlined,
|
'icon': Icons.payments_outlined,
|
||||||
'color': Colors.indigo
|
'color': Colors.indigo
|
||||||
},
|
},
|
||||||
|
// if (isSuperAdmin)
|
||||||
{
|
{
|
||||||
'title': 'محفظة انطلق',
|
'title': 'محفظة انطلق',
|
||||||
'value': _formatCurrency(data['seferWallet']),
|
'value': _formatCurrency(data['seferWallet']),
|
||||||
'icon': Icons.account_balance_wallet_outlined,
|
'icon': Icons.account_balance_wallet_outlined,
|
||||||
'color': Colors.deepOrange
|
'color': Colors.deepOrange
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
'title': 'عدد التحويلات',
|
'title': 'عدد التحويلات',
|
||||||
'value': data['transfer_from_count'].toString(),
|
'value': data['transfer_from_count'].toString(),
|
||||||
'icon': Icons.swap_horiz_outlined,
|
'icon': Icons.swap_horiz_outlined,
|
||||||
'color': Colors.brown
|
'color': Colors.brown
|
||||||
},
|
},
|
||||||
|
// ... بقية العناصر التي لا تحتاج إخفاء
|
||||||
{
|
{
|
||||||
'title': 'رحلات الصباح',
|
'title': 'رحلات الصباح',
|
||||||
'value': data['morning_ride_count'].toString(),
|
'value': data['morning_ride_count'].toString(),
|
||||||
@@ -248,43 +313,96 @@ class AdminHomePage extends StatelessWidget {
|
|||||||
'color': Colors.black87
|
'color': Colors.black87
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'نوع كومفورت',
|
'title': 'كومفورت',
|
||||||
'value': data['comfort'].toString(),
|
'value': data['comfort'].toString(),
|
||||||
'icon': Icons.event_seat_outlined,
|
'icon': Icons.event_seat_outlined,
|
||||||
'color': Colors.cyan
|
'color': Colors.cyan
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'نوع سبيد',
|
'title': 'سبيد',
|
||||||
'value': data['speed'].toString(),
|
'value': data['speed'].toString(),
|
||||||
'icon': Icons.speed_outlined,
|
'icon': Icons.speed_outlined,
|
||||||
'color': Colors.red.shade700
|
'color': Colors.red.shade700
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'نوع ليدي',
|
'title': 'ليدي',
|
||||||
'value': data['lady'].toString(),
|
'value': data['lady'].toString(),
|
||||||
'icon': Icons.woman_2_outlined,
|
'icon': Icons.woman_2_outlined,
|
||||||
'color': Colors.pink
|
'color': Colors.pink
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return AnimationLimiter(
|
return CustomScrollView(
|
||||||
child: ListView(
|
physics: const BouncingScrollPhysics(),
|
||||||
padding:
|
slivers: [
|
||||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
SliverAppBar(
|
||||||
|
expandedHeight: 120.0,
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
backgroundColor: AppColor.primaryColor,
|
||||||
|
elevation: 0,
|
||||||
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
|
titlePadding:
|
||||||
|
const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||||
|
title: const Text(
|
||||||
|
'لوحة التحكم',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: Colors.white),
|
||||||
|
),
|
||||||
|
background: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
AppColor.primaryColor,
|
||||||
|
AppColor.primaryColor.withOpacity(0.8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// --- Statistics Grid Section ---
|
Positioned(
|
||||||
AnimationLimiter(
|
left: -20,
|
||||||
child: GridView.builder(
|
top: -20,
|
||||||
|
child: Icon(Icons.dashboard,
|
||||||
|
size: 150, color: Colors.white.withOpacity(0.1)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async =>
|
||||||
|
await dashboardController.getDashBoard(),
|
||||||
|
icon: const Icon(Icons.refresh, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 20, 16, 10),
|
||||||
|
child: Text(
|
||||||
|
"نظرة عامة",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[800]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverGrid(
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: _calculateCrossAxisCount(context),
|
crossAxisCount: _calculateCrossAxisCount(context),
|
||||||
mainAxisSpacing: 12.0,
|
mainAxisSpacing: 12.0,
|
||||||
crossAxisSpacing: 12.0,
|
crossAxisSpacing: 12.0,
|
||||||
childAspectRatio: 1.8,
|
childAspectRatio: 1.6,
|
||||||
),
|
),
|
||||||
itemCount: statCards.length,
|
delegate: SliverChildBuilderDelegate(
|
||||||
shrinkWrap: true,
|
(context, index) {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final card = statCards[index];
|
final card = statCards[index];
|
||||||
return AnimationConfiguration.staggeredGrid(
|
return AnimationConfiguration.staggeredGrid(
|
||||||
position: index,
|
position: index,
|
||||||
@@ -292,184 +410,274 @@ class AdminHomePage extends StatelessWidget {
|
|||||||
columnCount: _calculateCrossAxisCount(context),
|
columnCount: _calculateCrossAxisCount(context),
|
||||||
child: ScaleAnimation(
|
child: ScaleAnimation(
|
||||||
child: FadeInAnimation(
|
child: FadeInAnimation(
|
||||||
child: DashboardStatCard(
|
child: _buildModernStatCard(
|
||||||
title: card['title'] as String,
|
title: card['title'],
|
||||||
value: card['value'].toString(),
|
value: card['value'],
|
||||||
icon: card['icon'] as IconData,
|
icon: card['icon'],
|
||||||
iconColor: card['color'] as Color,
|
color: card['color'],
|
||||||
valueColor: (card['color'] as Color),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
childCount: statCards.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
SliverToBoxAdapter(
|
||||||
Text("الإجراءات السريعة",
|
child: Padding(
|
||||||
style: Theme.of(context)
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 10),
|
||||||
.textTheme
|
child: Text(
|
||||||
.titleLarge
|
"إدارة النظام",
|
||||||
?.copyWith(fontWeight: FontWeight.bold)),
|
style: TextStyle(
|
||||||
const SizedBox(height: 10),
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
// --- Admin Actions List Section ---
|
color: Colors.grey[800]),
|
||||||
AnimationLimiter(
|
),
|
||||||
child: ListView.builder(
|
),
|
||||||
itemCount: actionItems.length,
|
),
|
||||||
shrinkWrap: true,
|
SliverPadding(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
padding:
|
||||||
itemBuilder: (context, index) {
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount:
|
||||||
|
_calculateCrossAxisCount(context, isSmall: true),
|
||||||
|
mainAxisSpacing: 12.0,
|
||||||
|
crossAxisSpacing: 12.0,
|
||||||
|
childAspectRatio: 1.0,
|
||||||
|
),
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
final item = actionItems[index];
|
final item = actionItems[index];
|
||||||
return AnimationConfiguration.staggeredList(
|
return AnimationConfiguration.staggeredGrid(
|
||||||
position: index,
|
position: index,
|
||||||
duration: const Duration(milliseconds: 375),
|
duration: const Duration(milliseconds: 375),
|
||||||
|
columnCount:
|
||||||
|
_calculateCrossAxisCount(context, isSmall: true),
|
||||||
child: SlideAnimation(
|
child: SlideAnimation(
|
||||||
verticalOffset: 50.0,
|
verticalOffset: 50.0,
|
||||||
child: FadeInAnimation(
|
child: FadeInAnimation(
|
||||||
child: AdminActionTile(
|
child: _buildActionCard(
|
||||||
title: item['title'] as String,
|
context,
|
||||||
icon: item['icon'] as IconData,
|
title: item['title'],
|
||||||
onPressed: item['onPressed'] as void Function(),
|
icon: item['icon'],
|
||||||
iconColor: item['iconColor'] as Color?,
|
color: item['color'],
|
||||||
|
onTap: item['onPressed'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
childCount: actionItems.length,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: 40)),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- بقية الدوال (buildModernStatCard, buildActionCard, showWhatsAppDialog) تبقى كما هي ---
|
||||||
|
Widget _buildModernStatCard({
|
||||||
|
required String title,
|
||||||
|
required String value,
|
||||||
|
required IconData icon,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.04),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
isleading: false,
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[900],
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionCard(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required IconData icon,
|
||||||
|
required Color color,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
elevation: 0,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [color.withOpacity(0.2), color.withOpacity(0.05)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 28, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showWhatsAppDialog(BuildContext context) {
|
void _showWhatsAppDialog(BuildContext context) {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
title: Text('تأكيد إرسال الرسائل؟'),
|
title: const Text('إرسال رسالة جماعية',
|
||||||
content: MyTextForm(
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.send, size: 40, color: Colors.green),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
MyTextForm(
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
label: 'الرسالة',
|
label: 'الرسالة',
|
||||||
hint: 'أدخل نص الرسالة هنا',
|
hint: 'اكتب الرسالة هنا...',
|
||||||
type: TextInputType.text,
|
type: TextInputType.text,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actionsPadding: const EdgeInsets.all(15),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_messageController.clear();
|
_messageController.clear();
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
child: Text('إلغاء'),
|
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColor.primaryColor),
|
backgroundColor: Colors.green,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.send, size: 18, color: Colors.white),
|
||||||
|
label: const Text('إرسال', style: TextStyle(color: Colors.white)),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (_messageController.text.isNotEmpty) {
|
if (_messageController.text.isNotEmpty) {
|
||||||
Get.back(); // Close dialog first
|
Get.back();
|
||||||
var driverPhones =
|
var driverPhones =
|
||||||
box.read(BoxName.tokensDrivers)['message'] as List?;
|
box.read(BoxName.tokensDrivers)['message'] as List?;
|
||||||
if (driverPhones == null || driverPhones.isEmpty) {
|
if (driverPhones == null || driverPhones.isEmpty) {
|
||||||
Get.snackbar('خطأ', 'لم يتم العثور على أرقام هواتف للسائقين.',
|
Get.snackbar('تنبيه', 'لا توجد بيانات اتصال للسائقين',
|
||||||
snackPosition: SnackPosition.BOTTOM);
|
backgroundColor: Colors.amber.withOpacity(0.5));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Get.snackbar('جاري الإرسال', 'بدأت عملية الإرسال في الخلفية...',
|
||||||
|
backgroundColor: Colors.blue.withOpacity(0.3));
|
||||||
|
|
||||||
for (var driverData in driverPhones) {
|
for (var driverData in driverPhones) {
|
||||||
if (driverData['phone'] != null) {
|
if (driverData['phone'] != null) {
|
||||||
await CRUD().sendWhatsAppAuth(
|
await CRUD().sendWhatsAppAuth(
|
||||||
driverData['phone'].toString(),
|
driverData['phone'].toString(),
|
||||||
_messageController.text,
|
_messageController.text,
|
||||||
);
|
);
|
||||||
// Random delay to avoid being flagged as spam
|
|
||||||
await Future.delayed(
|
await Future.delayed(
|
||||||
Duration(seconds: Random().nextInt(5) + 2));
|
Duration(seconds: Random().nextInt(3) + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_messageController.clear();
|
_messageController.clear();
|
||||||
Get.snackbar(
|
Get.snackbar('نجاح', 'تمت العملية بنجاح',
|
||||||
'نجاح',
|
backgroundColor: Colors.green.withOpacity(0.5));
|
||||||
'تم إرسال الرسائل بنجاح',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.green.shade100,
|
|
||||||
colorText: Colors.black,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text('إرسال', style: TextStyle(color: Colors.white)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
barrierDismissible: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renamed for clarity and improved design
|
|
||||||
class AdminActionTile extends StatelessWidget {
|
|
||||||
const AdminActionTile({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required this.onPressed,
|
|
||||||
required this.icon,
|
|
||||||
this.iconColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
final IconData icon;
|
|
||||||
final Color? iconColor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).cardColor,
|
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: onPressed,
|
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
|
||||||
child: Container(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 18.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
size: 26,
|
|
||||||
color: iconColor ?? AppColor.primaryColor,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: AppStyle.title.copyWith(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,223 +1,302 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
// تأكد من استيراد مكتبة الاتصال إذا أردت تفعيل زر الاتصال فعلياً
|
||||||
|
// import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/style.dart';
|
import '../../../controller/functions/launch.dart';
|
||||||
import '../../../controller/admin/captain_admin_controller.dart';
|
import '../../../main.dart';
|
||||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
|
||||||
import '../../widgets/elevated_btn.dart';
|
|
||||||
import '../../widgets/my_scafold.dart';
|
import '../../widgets/my_scafold.dart';
|
||||||
import '../../widgets/my_textField.dart';
|
import '../../widgets/my_textField.dart';
|
||||||
import '../../widgets/mycircular.dart';
|
import '../../widgets/mycircular.dart';
|
||||||
|
import '../../../constant/style.dart';
|
||||||
|
import '../../../controller/admin/captain_admin_controller.dart';
|
||||||
import 'captain_details.dart';
|
import 'captain_details.dart';
|
||||||
import 'form_captain.dart';
|
|
||||||
|
|
||||||
class Captain extends StatelessWidget {
|
class CaptainsPage extends StatelessWidget {
|
||||||
Captain({super.key});
|
CaptainsPage({super.key});
|
||||||
final CaptainAdminController captainAdminController =
|
|
||||||
|
final CaptainAdminController captainController =
|
||||||
Get.put(CaptainAdminController());
|
Get.put(CaptainAdminController());
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
|
||||||
|
// 🔴 هام جداً: قم بتغيير هذا المتغير بناءً على حالة تسجيل الدخول الحقيقية في تطبيقك
|
||||||
|
// مثال: bool isAdmin = Get.find<AuthController>().isAdmin;
|
||||||
|
// final bool isAdmin = true; // اجعلها false لتجربة وضع المستخدم العادي
|
||||||
|
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||||
|
|
||||||
|
// 2. تحديد من هو "السوبر أدمن" الذي يرى كل شيء
|
||||||
|
// يمكنك إضافة المزيد من الأرقام هنا باستخدام || أو قائمة
|
||||||
|
bool isSuperAdmin = false;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||||
|
|
||||||
return MyScafolld(
|
return MyScafolld(
|
||||||
title: 'Captain'.tr,
|
title: 'Search for Captain'.tr,
|
||||||
|
isleading: true,
|
||||||
body: [
|
body: [
|
||||||
GetBuilder<CaptainAdminController>(
|
Container(
|
||||||
builder: (captainAdminController) => Column(
|
height: MediaQuery.of(context).size.height, // لضمان أخذ المساحة
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
captainAdminController.isLoading
|
// --- شريط البحث المحسن ---
|
||||||
? const MyCircularProgressIndicator()
|
_buildSearchSection(context),
|
||||||
: Column(
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// --- قائمة النتائج ---
|
||||||
|
Expanded(
|
||||||
|
child: GetBuilder<CaptainAdminController>(
|
||||||
|
builder: (controller) {
|
||||||
|
if (controller.isLoading) {
|
||||||
|
return const Center(child: MyCircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.captainData['message'] == null ||
|
||||||
|
controller.captainData['message'].isEmpty) {
|
||||||
|
return _buildEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
itemCount: controller.captainData['message'].length,
|
||||||
|
separatorBuilder: (context, index) =>
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final captain =
|
||||||
|
controller.captainData['message'][index];
|
||||||
|
return _buildCaptainCard(context, captain);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// مساحة إضافية في الأسفل لتجنب تداخل المحتوى مع الحواف
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ودجت البحث ---
|
||||||
|
Widget _buildSearchSection(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
spreadRadius: 2,
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Expanded(
|
||||||
padding: const EdgeInsets.all(5),
|
child: MyTextForm(
|
||||||
|
controller: searchController,
|
||||||
|
label: 'Captain Phone Number'.tr,
|
||||||
|
hint: 'Enter phone number...'.tr,
|
||||||
|
type: TextInputType.phone,
|
||||||
|
// يمكنك إزالة الحواف من الـ TextField الأصلي إذا أردت ليتناسب مع الكونتينر
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.search, size: 28, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
final phone = searchController.text;
|
||||||
|
if (phone.isNotEmpty) {
|
||||||
|
captainController.find_driver_by_phone(phone);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error'.tr,
|
||||||
|
'Please enter a phone number to search.'.tr,
|
||||||
|
backgroundColor: Colors.red.withOpacity(0.8),
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- بطاقة الكابتن المحسنة ---
|
||||||
|
Widget _buildCaptainCard(BuildContext context, dynamic captain) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.08),
|
||||||
|
spreadRadius: 1,
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () {
|
||||||
|
Get.to(() => const CaptainDetailsPage(),
|
||||||
|
arguments: {'data': captain});
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// صورة الكابتن أو أيقونة
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).primaryColor.withOpacity(0.1),
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
|
||||||
|
// المعلومات النصية
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// الاسم
|
||||||
|
Text(
|
||||||
|
'${captain['first_name']} ${captain['last_name']}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
// رقم الهاتف (مع المنطق)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.phone_iphone,
|
||||||
|
size: 14, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_formatPhoneNumber(captain['phone'].toString()),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontFamily:
|
||||||
|
'monospace', // لجعل الأرقام والنجوم متناسقة
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// الإيميل (يظهر فقط للأدمن)
|
||||||
|
if (isSuperAdmin && captain['email'] != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.email_outlined,
|
||||||
|
size: 14, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
captain['email'],
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// أزرار الإجراءات
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
// زر الاتصال (فقط للأدمن)
|
||||||
|
if (isSuperAdmin)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
// منطق الاتصال
|
||||||
|
makePhoneCall('+' + captain['phone']);
|
||||||
|
// Get.snackbar(
|
||||||
|
// 'Call', 'Calling ${captain['phone']}...');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.call, color: Colors.green),
|
||||||
|
tooltip: 'Call Captain',
|
||||||
|
),
|
||||||
|
|
||||||
|
if (!isSuperAdmin)
|
||||||
|
const Icon(Icons.arrow_forward_ios,
|
||||||
|
size: 16, color: Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- دالة تنسيق الرقم (المنطق المطلوب) ---
|
||||||
|
String _formatPhoneNumber(String phone) {
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
return phone; // للأدمن: إظهار الرقم كاملاً
|
||||||
|
} else {
|
||||||
|
// للمستخدم العادي: إظهار آخر 4 أرقام فقط
|
||||||
|
if (phone.length <= 4) return phone;
|
||||||
|
String lastFour = phone.substring(phone.length - 4);
|
||||||
|
String maskedPart = '*' * (phone.length - 4);
|
||||||
|
return '$maskedPart$lastFour'; // النتيجة: *******1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- تصميم الحالة الفارغة ---
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
captainAdmin(
|
Icon(Icons.search_off_rounded, size: 80, color: Colors.grey[300]),
|
||||||
captainAdminController,
|
const SizedBox(height: 10),
|
||||||
'Captains Count',
|
|
||||||
'countPassenger',
|
|
||||||
),
|
|
||||||
MyElevatedButton(
|
|
||||||
title: 'Add Prize to Gold Captains',
|
|
||||||
onPressed: () {
|
|
||||||
var date = DateTime.now();
|
|
||||||
var day = date.weekday;
|
|
||||||
|
|
||||||
if (day == 6) {
|
|
||||||
// Saturday is 6
|
|
||||||
Get.defaultDialog(
|
|
||||||
title:
|
|
||||||
'Add Prize to Gold Captains',
|
|
||||||
titleStyle: AppStyle.title,
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
Text(
|
||||||
'Add Points to their wallet as prize'
|
'No captains found.'.tr,
|
||||||
.tr,
|
style: TextStyle(
|
||||||
style: AppStyle.title,
|
fontSize: 18,
|
||||||
),
|
fontWeight: FontWeight.w600,
|
||||||
Form(
|
color: Colors.grey[500],
|
||||||
key: captainAdminController
|
|
||||||
.formCaptainPrizeKey,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller:
|
|
||||||
captainAdminController
|
|
||||||
.captainPrizeController,
|
|
||||||
label:
|
|
||||||
'Count of prize'
|
|
||||||
.tr,
|
|
||||||
hint: 'Count of prize'
|
|
||||||
.tr,
|
|
||||||
type: TextInputType
|
|
||||||
.number))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'Add',
|
|
||||||
onPressed: () async {
|
|
||||||
if (captainAdminController
|
|
||||||
.formCaptainPrizeKey
|
|
||||||
.currentState!
|
|
||||||
.validate()) {
|
|
||||||
captainAdminController
|
|
||||||
.addCaptainsPrizeToWalletSecure();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Get.defaultDialog(
|
|
||||||
title:
|
|
||||||
'This day is not allowed',
|
|
||||||
titleStyle: AppStyle.title,
|
|
||||||
middleText:
|
|
||||||
'Saturday only Allowed day',
|
|
||||||
middleTextStyle: AppStyle.title,
|
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'Ok'.tr,
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
InkWell(
|
|
||||||
onTap: () {
|
|
||||||
//todo search
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(3),
|
|
||||||
child: Container(
|
|
||||||
width: Get.width,
|
|
||||||
height: 110,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
width: 2,
|
|
||||||
color: AppColor.greenColor)),
|
|
||||||
child: formSearchCaptain()
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: Get.height * .5,
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: captainAdminController
|
|
||||||
.captainData['message'].length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final user = captainAdminController
|
|
||||||
.captainData['message'][index];
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
|
||||||
Get.to(const CaptainsDetailsPage(),
|
|
||||||
arguments: {
|
|
||||||
'data': user,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(3),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(width: 2)),
|
|
||||||
child: ListTile(
|
|
||||||
title: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment
|
|
||||||
.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Name : ${(user['first_name'])} ${(user['last_name'])}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Rating : ${user['ratingPassenger']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment
|
|
||||||
.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Count Trip : ${user['countPassengerRide']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Count Driver Rate : ${user['countDriverRate']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
))
|
|
||||||
],
|
|
||||||
isleading: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Container captainAdmin(CaptainAdminController captainAdminController,
|
|
||||||
String title, String jsonField) {
|
|
||||||
return Container(
|
|
||||||
height: Get.height * .1,
|
|
||||||
decoration: BoxDecoration(border: Border.all(width: 2)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {},
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title.tr,
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
captainAdminController.captainData['message'][0][jsonField]
|
|
||||||
.toString(),
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,167 +1,404 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/style.dart';
|
|
||||||
import '../../../controller/admin/captain_admin_controller.dart';
|
import '../../../controller/admin/captain_admin_controller.dart';
|
||||||
import '../../../controller/firebase/firbase_messge.dart';
|
import '../../../controller/firebase/firbase_messge.dart';
|
||||||
|
import '../../../main.dart'; // Import main to access myPhone
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
import '../../widgets/my_scafold.dart';
|
import '../../widgets/my_scafold.dart';
|
||||||
import '../../widgets/my_textField.dart';
|
import '../../widgets/my_textField.dart';
|
||||||
|
import 'form_captain.dart';
|
||||||
|
|
||||||
class CaptainsDetailsPage extends StatelessWidget {
|
class CaptainDetailsPage extends StatelessWidget {
|
||||||
const CaptainsDetailsPage({super.key});
|
const CaptainDetailsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final arguments = Get.arguments;
|
final Map<String, dynamic> data = Get.arguments['data'];
|
||||||
final Map<String, dynamic> data = arguments['data'];
|
final controller = Get.find<CaptainAdminController>();
|
||||||
var key = Get.find<CaptainAdminController>().formCaptainPrizeKey;
|
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||||
var titleNotify = Get.find<CaptainAdminController>().titleNotify;
|
|
||||||
var bodyNotify = Get.find<CaptainAdminController>().bodyNotify;
|
// Define Super Admin Logic
|
||||||
|
final bool isSuperAdmin =
|
||||||
|
myPhone == '963942542053' || myPhone == '963992952235';
|
||||||
|
|
||||||
return MyScafolld(
|
return MyScafolld(
|
||||||
title: data['first_name'] + ' ' + data['last_name'],
|
title: 'Captain Profile'.tr,
|
||||||
|
isleading: true,
|
||||||
body: [
|
body: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 40),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// --- Header Section (Avatar & Name) ---
|
||||||
|
_buildHeaderSection(context, data),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// --- Personal Information Card ---
|
||||||
|
_buildInfoCard(
|
||||||
|
title: 'Personal Information',
|
||||||
|
icon: Icons.person,
|
||||||
|
children: [
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.email_outlined,
|
||||||
|
'Email',
|
||||||
|
isSuperAdmin
|
||||||
|
? data['email']
|
||||||
|
: _maskEmail(
|
||||||
|
data['email']) // Mask email for non-super
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.phone_iphone,
|
||||||
|
'Phone',
|
||||||
|
_formatPhoneNumber(
|
||||||
|
data['phone'].toString(), isSuperAdmin)),
|
||||||
|
_buildDetailTile(Icons.transgender, 'Gender',
|
||||||
|
data['gender'] ?? 'Not specified'),
|
||||||
|
_buildDetailTile(Icons.cake_outlined, 'Birthdate',
|
||||||
|
data['birthdate'] ?? 'N/A'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// --- Ride Statistics Card ---
|
||||||
|
_buildInfoCard(
|
||||||
|
title: 'Performance & Stats',
|
||||||
|
icon: Icons.bar_chart_rounded,
|
||||||
|
children: [
|
||||||
|
_buildDetailTile(Icons.star_rate_rounded, 'Rating',
|
||||||
|
'${data['ratingPassenger'] ?? 0.0} / 5.0',
|
||||||
|
valueColor: Colors.amber[700]),
|
||||||
|
_buildDetailTile(Icons.directions_car_filled_outlined,
|
||||||
|
'Total Rides', data['countPassengerRide']),
|
||||||
|
_buildDetailTile(Icons.cancel_outlined,
|
||||||
|
'Canceled Rides', data['countPassengerCancel'],
|
||||||
|
valueColor: Colors.redAccent),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// --- Action Buttons ---
|
||||||
|
_buildActionButtons(
|
||||||
|
context, controller, data, isSuperAdmin),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Header with Gradient Background ---
|
||||||
|
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 25),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(30)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 45,
|
||||||
|
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||||
|
child: Text(
|
||||||
|
data['first_name'] != null
|
||||||
|
? data['first_name'][0].toUpperCase()
|
||||||
|
: 'C',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 35,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'${data['first_name']} ${data['last_name']}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Active Captain'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.green,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoCard(
|
||||||
|
{required String title,
|
||||||
|
required IconData icon,
|
||||||
|
required List<Widget> children}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.05),
|
||||||
|
spreadRadius: 2,
|
||||||
|
blurRadius: 10)
|
||||||
|
],
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'Email is ${data['email']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Icon(icon, color: AppColor.primaryColor, size: 22),
|
||||||
'Phone is ${data['phone']}',
|
const SizedBox(width: 10),
|
||||||
style: AppStyle.title,
|
Text(title.tr,
|
||||||
),
|
style: const TextStyle(
|
||||||
Text(
|
fontSize: 17, fontWeight: FontWeight.bold)),
|
||||||
'gender is ${data['gender']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Divider(height: 25, color: Colors.grey.withOpacity(0.2)),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
...children,
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'status is ${data['status']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'birthdate is ${data['birthdate']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
);
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailTile(IconData icon, String label, dynamic value,
|
||||||
|
{Color? valueColor}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'site is ${data['site']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
// Text(
|
|
||||||
// 'sosPhone is ${data['sosPhone']}',
|
|
||||||
// style: AppStyle.title,
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Count Feedback is ${data['countFeedback']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Count Driver Rate is ${data['countDriverRate']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Count Cancel is ${data['countPassengerCancel']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Count Ride is ${data['countPassengerRide']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Rating Captain Avarage is ${data['passengerAverageRating']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Rating is ${data['ratingPassenger']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(width: 3, color: AppColor.yellowColor)),
|
color: Colors.grey[100],
|
||||||
child: TextButton(
|
borderRadius: BorderRadius.circular(8)),
|
||||||
onPressed: () async {
|
child: Icon(icon, color: Colors.grey[600], size: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label.tr,
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[500])),
|
||||||
|
Text(
|
||||||
|
value?.toString() ?? 'N/A',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: valueColor ?? Colors.black87),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButtons(
|
||||||
|
BuildContext context,
|
||||||
|
CaptainAdminController controller,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
bool isSuperAdmin) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Notification is available for everyone
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.notifications_active_outlined,
|
||||||
|
color: Colors.white),
|
||||||
|
label: Text("Send Notification".tr,
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColor.primaryColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
onPressed: () => _showSendNotificationDialog(controller, data),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Edit and Delete ONLY for Super Admin
|
||||||
|
if (isSuperAdmin) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.edit_note_rounded, size: 20),
|
||||||
|
label: Text("Edit".tr),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: AppColor.yellowColor,
|
||||||
|
elevation: 0,
|
||||||
|
side: BorderSide(color: AppColor.yellowColor),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Get.to(() => const FormCaptain(), arguments: {
|
||||||
|
'isEditMode': true,
|
||||||
|
'captainData': data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.delete_outline_rounded, size: 20),
|
||||||
|
label: Text("Delete".tr),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red[50],
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: () => _showDeleteConfirmation(data),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
// Message for normal admins
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
Text(
|
||||||
|
"Only Super Admins can edit or delete captains.",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[400],
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: FontStyle.italic),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper Methods ---
|
||||||
|
|
||||||
|
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
|
||||||
|
if (isSuperAdmin) return phone;
|
||||||
|
if (phone.length <= 4) return phone;
|
||||||
|
return '${'*' * (phone.length - 4)}${phone.substring(phone.length - 4)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _maskEmail(String? email) {
|
||||||
|
if (email == null || email.isEmpty) return 'N/A';
|
||||||
|
int atIndex = email.indexOf('@');
|
||||||
|
if (atIndex <= 1) return email; // Too short to mask
|
||||||
|
return '${email.substring(0, 2)}****${email.substring(atIndex)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSendNotificationDialog(
|
||||||
|
CaptainAdminController controller, Map<String, dynamic> data) {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'Send Notification'.tr,
|
title: 'Send Notification'.tr,
|
||||||
titleStyle: AppStyle.title,
|
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: key,
|
key: controller.formCaptainPrizeKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: titleNotify,
|
controller: controller.titleNotify,
|
||||||
label: 'title'.tr,
|
label: 'Title'.tr,
|
||||||
hint: 'title notificaton'.tr,
|
hint: 'Enter notification title'.tr,
|
||||||
type: TextInputType.name),
|
type: TextInputType.text,
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: bodyNotify,
|
controller: controller.bodyNotify,
|
||||||
label: 'body'.tr,
|
label: 'Body'.tr,
|
||||||
hint: 'body notificaton'.tr,
|
hint: 'Enter message body'.tr,
|
||||||
type: TextInputType.name)
|
type: TextInputType.text,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
confirm: MyElevatedButton(
|
confirm: SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: MyElevatedButton(
|
||||||
title: 'Send',
|
title: 'Send',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (key.currentState!.validate()) {
|
// Check if key is valid (might be recreated)
|
||||||
FirebaseMessagesController()
|
if (controller.formCaptainPrizeKey.currentState?.validate() ??
|
||||||
.sendNotificationToAnyWithoutData(
|
true) {
|
||||||
titleNotify.text,
|
FirebaseMessagesController().sendNotificationToAnyWithoutData(
|
||||||
bodyNotify.text,
|
controller.titleNotify.text,
|
||||||
data['passengerToken'],
|
controller.bodyNotify.text,
|
||||||
|
data['passengerToken'] ?? '', // Safety check
|
||||||
'order.wav');
|
'order.wav');
|
||||||
Get.back();
|
Get.back();
|
||||||
|
Get.snackbar("Success", "Notification Sent",
|
||||||
|
backgroundColor: Colors.green.withOpacity(0.2));
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
child: Text(
|
|
||||||
"Send Notificaion to Captains ".tr,
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
cancel: TextButton(
|
||||||
],
|
onPressed: () => Get.back(),
|
||||||
|
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDeleteConfirmation(Map<String, dynamic> user) {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: 'Confirm Deletion'.tr,
|
||||||
|
titleStyle:
|
||||||
|
const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
|
||||||
|
middleText:
|
||||||
|
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
|
||||||
|
.tr,
|
||||||
|
confirm: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
||||||
|
onPressed: () {
|
||||||
|
// Call delete function here
|
||||||
|
// controller.deleteCaptain(user['id']);
|
||||||
|
Get.back();
|
||||||
|
Get.snackbar("Deleted", "Captain has been removed",
|
||||||
|
backgroundColor: Colors.red.withOpacity(0.2));
|
||||||
|
},
|
||||||
|
child: Text('Delete'.tr, style: const TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
)
|
cancel: TextButton(
|
||||||
],
|
onPressed: () => Get.back(),
|
||||||
isleading: true,
|
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
lib/views/admin/captain/driver_details_not_active_page.dart
Normal file
81
lib/views/admin/captain/driver_details_not_active_page.dart
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../controller/drivers/driver_not_active_controller.dart';
|
||||||
|
|
||||||
|
class DriverDetailsPage extends StatelessWidget {
|
||||||
|
final String driverId;
|
||||||
|
final DriverController controller = Get.find();
|
||||||
|
|
||||||
|
DriverDetailsPage({super.key, required this.driverId});
|
||||||
|
|
||||||
|
/// Helper function to safely get String values from dynamic map
|
||||||
|
String safeVal(Map d, String key) {
|
||||||
|
final v = d[key];
|
||||||
|
if (v == null || v == false) return '';
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
controller.getDriverDetails(driverId);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("Driver Details")),
|
||||||
|
body: GetBuilder<DriverController>(
|
||||||
|
id: 'driverDetails',
|
||||||
|
builder: (c) {
|
||||||
|
if (c.driverDetails.isEmpty) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final d = c.driverDetails['driver'] as Map;
|
||||||
|
final docs = c.driverDetails['documents'] as List;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Name: ${safeVal(d, 'first_name')} ${safeVal(d, 'last_name')}"),
|
||||||
|
Text("Phone: ${safeVal(d, 'phone')}"),
|
||||||
|
Text("Email: ${safeVal(d, 'email')}"),
|
||||||
|
Text("National Number: ${safeVal(d, 'national_number')}"),
|
||||||
|
Text("Gender: ${safeVal(d, 'gender')}"),
|
||||||
|
Text("Birthdate: ${safeVal(d, 'birthdate')}"),
|
||||||
|
Text("Status: ${safeVal(d, 'status')}"),
|
||||||
|
Text("License Type: ${safeVal(d, 'license_type')}"),
|
||||||
|
Text("License Categories: ${safeVal(d, 'license_categories')}"),
|
||||||
|
Text("Issue Date: ${safeVal(d, 'issue_date')}"),
|
||||||
|
Text("Expiry Date: ${safeVal(d, 'expiry_date')}"),
|
||||||
|
Text("Address: ${safeVal(d, 'address')}"),
|
||||||
|
Text("Site: ${safeVal(d, 'site')}"),
|
||||||
|
Text("Employment Type: ${safeVal(d, 'employmentType')}"),
|
||||||
|
Text("Marital Status: ${safeVal(d, 'maritalStatus')}"),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text("Documents:",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...docs.map((doc) => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(safeVal(doc, "doc_type")),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Image.network(
|
||||||
|
safeVal(doc, "link"),
|
||||||
|
height: 200,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (ctx, err, st) =>
|
||||||
|
const Icon(Icons.broken_image),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,84 +1,122 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../constant/colors.dart';
|
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||||
|
import '../../../constant/links.dart';
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/admin/captain_admin_controller.dart';
|
import '../../../controller/admin/captain_admin_controller.dart';
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
import 'captain_details.dart';
|
import '../../widgets/my_scafold.dart';
|
||||||
|
import '../../widgets/my_textField.dart';
|
||||||
|
|
||||||
GetBuilder<CaptainAdminController> formSearchCaptain() {
|
class FormCaptain extends StatefulWidget {
|
||||||
// DbSql sql = DbSql.instance;
|
const FormCaptain({super.key});
|
||||||
return GetBuilder<CaptainAdminController>(
|
|
||||||
builder: (controller) => Column(
|
@override
|
||||||
children: [
|
State<FormCaptain> createState() => _FormCaptainState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FormCaptainState extends State<FormCaptain> {
|
||||||
|
final CaptainAdminController controller = Get.find();
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
late TextEditingController firstNameController;
|
||||||
|
late TextEditingController lastNameController;
|
||||||
|
late TextEditingController phoneController;
|
||||||
|
|
||||||
|
bool isEditMode = false;
|
||||||
|
Map<String, dynamic>? captainData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (Get.arguments != null && Get.arguments['isEditMode'] == true) {
|
||||||
|
isEditMode = true;
|
||||||
|
captainData = Get.arguments['captainData'];
|
||||||
|
firstNameController =
|
||||||
|
TextEditingController(text: captainData?['first_name'] ?? '');
|
||||||
|
lastNameController =
|
||||||
|
TextEditingController(text: captainData?['last_name'] ?? '');
|
||||||
|
phoneController =
|
||||||
|
TextEditingController(text: captainData?['phone'] ?? '');
|
||||||
|
} else {
|
||||||
|
firstNameController = TextEditingController();
|
||||||
|
lastNameController = TextEditingController();
|
||||||
|
phoneController = TextEditingController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
firstNameController.dispose();
|
||||||
|
lastNameController.dispose();
|
||||||
|
phoneController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveForm() async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
// Create a map of the updated data
|
||||||
|
Map<String, dynamic> updatedData = {
|
||||||
|
'id': captainData?['id'], // Important for the WHERE clause in SQL
|
||||||
|
// 'first_name': firstNameController.text,
|
||||||
|
// 'last_name': lastNameController.text,
|
||||||
|
'phone': phoneController.text,
|
||||||
|
};
|
||||||
|
var res = await CRUD()
|
||||||
|
.post(link: AppLink.updateDriverFromAdmin, payload: updatedData);
|
||||||
|
|
||||||
|
if (res != 'failure') {
|
||||||
|
print('Updating data: $updatedData');
|
||||||
|
|
||||||
|
Get.back(); // Go back after saving
|
||||||
|
Get.snackbar('Success', 'Captain data updated successfully!');
|
||||||
|
}
|
||||||
|
// controller.updateCaptain(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MyScafolld(
|
||||||
|
title: 'Edit Captain'.tr,
|
||||||
|
isleading: true,
|
||||||
|
body: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Container(
|
child: Form(
|
||||||
decoration:
|
key: _formKey,
|
||||||
const BoxDecoration(color: AppColor.secondaryColor),
|
child: ListView(
|
||||||
child: TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: const OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.only(),
|
|
||||||
gapPadding: 4,
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: AppColor.redColor,
|
|
||||||
width: 2,
|
|
||||||
)),
|
|
||||||
suffixIcon: InkWell(
|
|
||||||
onTap: () async {
|
|
||||||
if (controller.captainController.text.length > 4) {
|
|
||||||
await controller.getCaptains();
|
|
||||||
|
|
||||||
Get.defaultDialog(
|
|
||||||
title: controller.captain['message'][0]
|
|
||||||
['email'],
|
|
||||||
titleStyle: AppStyle.title,
|
|
||||||
content: Column(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
MyTextForm(
|
||||||
'Name is ${controller.captain['message'][0]['first_name']} ${controller.captain['message'][0]['last_name']}',
|
controller: firstNameController,
|
||||||
style: AppStyle.title,
|
label: 'First Name'.tr,
|
||||||
|
hint: 'Enter first name'.tr,
|
||||||
|
type: TextInputType.name,
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(height: 16),
|
||||||
'phone is ${controller.captain['message'][0]['phone']}',
|
MyTextForm(
|
||||||
style: AppStyle.title,
|
controller: lastNameController,
|
||||||
|
label: 'Last Name'.tr,
|
||||||
|
hint: 'Enter last name'.tr,
|
||||||
|
type: TextInputType.name,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
MyTextForm(
|
||||||
|
controller: phoneController,
|
||||||
|
label: 'Phone Number'.tr,
|
||||||
|
hint: 'Enter phone number'.tr,
|
||||||
|
type: TextInputType.phone,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
MyElevatedButton(
|
||||||
|
title: 'Update'.tr,
|
||||||
|
onPressed: _saveForm,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'Go To Details'.tr,
|
|
||||||
onPressed: () {
|
|
||||||
Get.to(
|
|
||||||
() => const CaptainsDetailsPage(),
|
|
||||||
arguments: {
|
|
||||||
'data': controller
|
|
||||||
.captain['message'][0],
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.search)),
|
|
||||||
hintText: 'Search for Passenger'.tr,
|
|
||||||
hintStyle: AppStyle.title,
|
|
||||||
hintMaxLines: 1,
|
|
||||||
prefixIcon: IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
controller.captainController.clear();
|
|
||||||
// controller.clearPlaces();
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
Icons.clear,
|
|
||||||
color: Colors.red[300],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
controller: controller.captainController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
));
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
lib/views/admin/captain/syrian_driver_not_active.dart
Normal file
54
lib/views/admin/captain/syrian_driver_not_active.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||||
|
import 'package:sefer_admin1/constant/box_name.dart';
|
||||||
|
|
||||||
|
import '../../../constant/info.dart';
|
||||||
|
import '../../../constant/char_map.dart';
|
||||||
|
|
||||||
|
import '../../../controller/drivers/driver_not_active_controller.dart';
|
||||||
|
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||||
|
import '../../../main.dart';
|
||||||
|
import '../../../print.dart';
|
||||||
|
import 'driver_details_not_active_page.dart';
|
||||||
|
|
||||||
|
class DriversPendingPage extends StatelessWidget {
|
||||||
|
final DriverController controller = Get.put(DriverController());
|
||||||
|
|
||||||
|
DriversPendingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
controller.getDriversPending();
|
||||||
|
Log.print(
|
||||||
|
': ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}');
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("Drivers Pending")),
|
||||||
|
body: GetBuilder<DriverController>(
|
||||||
|
id: 'drivers',
|
||||||
|
builder: (c) {
|
||||||
|
if (c.drivers.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: const Text('no drivers found yet',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: c.drivers.length,
|
||||||
|
itemBuilder: (ctx, i) {
|
||||||
|
final d = c.drivers[i];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(d["first_name"] + d['last_name'] ?? ""),
|
||||||
|
subtitle: Text(d["phone"] ?? ""),
|
||||||
|
onTap: () {
|
||||||
|
Get.to(() => DriverDetailsPage(driverId: d["id"].toString()));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
219
lib/views/admin/drivers/driver_gift_check_page.dart
Normal file
219
lib/views/admin/drivers/driver_gift_check_page.dart
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||||
|
import 'package:sefer_admin1/controller/functions/wallet.dart'; // تأكد من المسار
|
||||||
|
|
||||||
|
// --- Controller: المسؤول عن المنطق (البحث، الفحص، الإضافة) ---
|
||||||
|
class DriverGiftCheckerController extends GetxController {
|
||||||
|
// للتحكم في حقل النص
|
||||||
|
final TextEditingController phoneController = TextEditingController();
|
||||||
|
|
||||||
|
// لعرض النتائج وحالة التحميل
|
||||||
|
var statusLog = "".obs;
|
||||||
|
var isLoading = false.obs;
|
||||||
|
|
||||||
|
// قائمة السائقين (سنقوم بتحميلها للبحث عن الـ ID)
|
||||||
|
List<dynamic> driversCache = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
// fetchDriverCache(); // تحميل البيانات عند فتح الصفحة
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. تحميل قائمة السائقين لاستخراج الـ ID منها
|
||||||
|
Future<void> fetchDriverCache() async {
|
||||||
|
try {
|
||||||
|
final response = await CRUD().post(
|
||||||
|
link:
|
||||||
|
'https://api.intaleq.xyz/intaleq/Admin/driver/getDriverGiftPayment.php',
|
||||||
|
payload: {'phone': phoneController.text.trim()},
|
||||||
|
);
|
||||||
|
// print('response: ${response}');
|
||||||
|
|
||||||
|
if (response != 'failure') {
|
||||||
|
driversCache = (response['message']);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error loading cache: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- الدالة الرئيسية التي تنفذ العملية المطلوبة ---
|
||||||
|
Future<void> processDriverGift() async {
|
||||||
|
String phoneInput = phoneController.text.trim();
|
||||||
|
|
||||||
|
if (phoneInput.isEmpty) {
|
||||||
|
Get.snackbar("تنبيه", "يرجى إدخال رقم الهاتف",
|
||||||
|
backgroundColor: Colors.orange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fetchDriverCache();
|
||||||
|
isLoading.value = true;
|
||||||
|
statusLog.value = "جاري البحث عن السائق...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// الخطوة 1: استخراج الـ ID بناءً على رقم الهاتف
|
||||||
|
var driver = driversCache.firstWhere(
|
||||||
|
(d) => d['phone'].toString().contains(phoneInput),
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (driver == null) {
|
||||||
|
statusLog.value = "❌ لم يتم العثور على سائق بهذا الرقم في الكاش.";
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String driverId = driver['id'].toString();
|
||||||
|
String driverName = driver['name_arabic'] ?? 'بدون اسم';
|
||||||
|
|
||||||
|
statusLog.value =
|
||||||
|
"✅ تم العثور على السائق: $driverName (ID: $driverId)\nجاري فحص رصيد الهدايا...";
|
||||||
|
|
||||||
|
// الخطوة 2: فحص السيرفر هل الهدية موجودة؟
|
||||||
|
// bool hasGift = await _checkIfGiftExistsOnServer(driverId);
|
||||||
|
|
||||||
|
// if (hasGift) {
|
||||||
|
// statusLog.value +=
|
||||||
|
// "\n⚠️ هذا السائق لديه هدية الافتتاح (30,000) مسبقاً. لم يتم اتخاذ إجراء.";
|
||||||
|
// } else {
|
||||||
|
// الخطوة 3: إضافة الهدية
|
||||||
|
statusLog.value += "\n🎁 الهدية غير موجودة. جاري الإضافة...";
|
||||||
|
await _addGiftToDriver(driverId, phoneInput, "30000");
|
||||||
|
// }
|
||||||
|
} catch (e) {
|
||||||
|
statusLog.value = "حدث خطأ غير متوقع: $e";
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة إضافة الهدية باستخدام WalletController الموجود عندك
|
||||||
|
Future<void> _addGiftToDriver(
|
||||||
|
String driverId, String phone, String amount) async {
|
||||||
|
final wallet = Get.put(WalletController());
|
||||||
|
|
||||||
|
// استخدام الدالة الموجودة في نظامك
|
||||||
|
await wallet.addDrivergift3000('new driver', driverId, amount, phone);
|
||||||
|
|
||||||
|
// statusLog.value += "\n✅ تمت إضافة مبلغ $amount ل.س بنجاح!";
|
||||||
|
|
||||||
|
// إضافة تنبيه مرئي
|
||||||
|
// Get.snackbar("تم بنجاح", "تمت إضافة هدية الافتتاح للسائق",
|
||||||
|
// backgroundColor: Colors.green, colorText: Colors.white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- View: واجهة المستخدم ---
|
||||||
|
class DriverGiftCheckPage extends StatelessWidget {
|
||||||
|
const DriverGiftCheckPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// حقن الكنترولر
|
||||||
|
final controller = Get.put(DriverGiftCheckerController());
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF8FAFC),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("فحص ومنح هدية الافتتاح",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
backgroundColor: const Color(0xFF0F172A), // نفس لون الهيدر السابق
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// كارد الإدخال
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 10)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.card_giftcard,
|
||||||
|
size: 50, color: Colors.amber),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
"أدخل رقم الهاتف للتحقق",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// حقل الإدخال
|
||||||
|
TextField(
|
||||||
|
controller: controller.phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'مثال: 0912345678',
|
||||||
|
prefixIcon: const Icon(Icons.phone),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey[50],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// زر التنفيذ
|
||||||
|
Obx(() => SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: () => controller.processDriverGift(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF0F172A),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: controller.isLoading.value
|
||||||
|
? const CircularProgressIndicator(
|
||||||
|
color: Colors.white)
|
||||||
|
: const Text("تحقق ومنح الهدية (30,000)",
|
||||||
|
style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// منطقة عرض النتائج (Log)
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black87,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Obx(() => Text(
|
||||||
|
controller.statusLog.value.isEmpty
|
||||||
|
? "بانتظار العملية..."
|
||||||
|
: controller.statusLog.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.greenAccent,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
height: 1.5),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,103 +1,654 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_admin1/constant/links.dart';
|
import 'package:get_storage/get_storage.dart'; // Ensure get_storage is in pubspec.yaml
|
||||||
import 'package:sefer_admin1/controller/functions/crud.dart';
|
|
||||||
import 'package:sefer_admin1/controller/functions/encrypt_decrypt.dart';
|
|
||||||
import 'package:sefer_admin1/controller/functions/wallet.dart';
|
import 'package:sefer_admin1/controller/functions/wallet.dart';
|
||||||
import 'package:sefer_admin1/views/widgets/elevated_btn.dart';
|
|
||||||
import 'package:sefer_admin1/views/widgets/my_scafold.dart';
|
|
||||||
|
|
||||||
import '../../../controller/drivers/driverthebest.dart';
|
// --- New Controller to handle the specific JSON URL ---
|
||||||
import 'alexandria.dart';
|
class DriverCacheController extends GetxController {
|
||||||
import 'giza.dart';
|
List<dynamic> drivers = [];
|
||||||
|
bool isLoading = false;
|
||||||
|
String lastUpdated = '';
|
||||||
|
String searchQuery = ''; // Search query state
|
||||||
|
|
||||||
class DriverTheBest extends StatelessWidget {
|
// Storage for paid drivers
|
||||||
const DriverTheBest({super.key});
|
final box = GetStorage();
|
||||||
|
List<String> paidDrivers = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
// Load previously paid drivers from storage
|
||||||
|
var stored = box.read('paid_drivers');
|
||||||
|
if (stored != null) {
|
||||||
|
paidDrivers = List<String>.from(stored.map((e) => e.toString()));
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchData() async {
|
||||||
|
isLoading = true;
|
||||||
|
update(); // Notify UI to show loader
|
||||||
|
try {
|
||||||
|
// Using GetConnect to fetch the JSON directly
|
||||||
|
final response = await GetConnect().get(
|
||||||
|
'https://api.intaleq.xyz/intaleq/ride/location/active_drivers_cache.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.body != null && response.body is Map) {
|
||||||
|
if (response.body['data'] != null) {
|
||||||
|
drivers = List<dynamic>.from(response.body['data']);
|
||||||
|
}
|
||||||
|
if (response.body['last_updated'] != null) {
|
||||||
|
lastUpdated = response.body['last_updated'].toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error fetching driver cache: $e");
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
update(); // Update UI with data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update search query
|
||||||
|
void updateSearchQuery(String query) {
|
||||||
|
searchQuery = query;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark driver as paid and save to storage
|
||||||
|
void markAsPaid(String driverId) {
|
||||||
|
// Validation: Don't mark if ID is invalid
|
||||||
|
if (driverId == 'null' || driverId.isEmpty) return;
|
||||||
|
|
||||||
|
if (!paidDrivers.contains(driverId)) {
|
||||||
|
paidDrivers.add(driverId);
|
||||||
|
box.write('paid_drivers', paidDrivers);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all paid status (Delete Box)
|
||||||
|
void clearPaidStorage() {
|
||||||
|
paidDrivers.clear();
|
||||||
|
box.remove('paid_drivers');
|
||||||
|
update();
|
||||||
|
Get.snackbar(
|
||||||
|
"Storage Cleared",
|
||||||
|
"Paid status history has been reset",
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if driver is already paid
|
||||||
|
bool isDriverPaid(String driverId) {
|
||||||
|
return paidDrivers.contains(driverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriverTheBestRedesigned extends StatelessWidget {
|
||||||
|
const DriverTheBestRedesigned({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(Driverthebest(), permanent: true);
|
// Put the new controller
|
||||||
return MyScafolld(
|
final controller = Get.put(DriverCacheController());
|
||||||
title: 'Best Drivers'.tr,
|
|
||||||
body: [
|
return Scaffold(
|
||||||
GetBuilder<Driverthebest>(builder: (driverthebest) {
|
backgroundColor: const Color(0xFFF8FAFC), // slate-50 background
|
||||||
return driverthebest.driver.isNotEmpty
|
body: SafeArea(
|
||||||
? Column(
|
child: GetBuilder<DriverCacheController>(builder: (ctrl) {
|
||||||
|
if (ctrl.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter List based on Search Query
|
||||||
|
List<dynamic> filteredDrivers = ctrl.drivers.where((driver) {
|
||||||
|
if (ctrl.searchQuery.isEmpty) return true;
|
||||||
|
final phone = driver['phone']?.toString() ?? '';
|
||||||
|
// Simple contains check for phone
|
||||||
|
return phone.contains(ctrl.searchQuery);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Sort by Active Time (Hours) Descending
|
||||||
|
// We use the filtered list for sorting and display
|
||||||
|
filteredDrivers.sort((a, b) {
|
||||||
|
double hoursA = _calculateHoursFromStr(a['active_time']);
|
||||||
|
double hoursB = _calculateHoursFromStr(b['active_time']);
|
||||||
|
return hoursB.compareTo(hoursA);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 1. Calculate Stats (Based on ALL drivers, not just filtered, to keep dashboard stable) ---
|
||||||
|
int totalDrivers = ctrl.drivers.length;
|
||||||
|
int eliteCount = 0;
|
||||||
|
int inactiveCount = 0;
|
||||||
|
double maxTime = 0.0;
|
||||||
|
|
||||||
|
for (var driver in ctrl.drivers) {
|
||||||
|
double hours = _calculateHoursFromStr(driver['active_time']);
|
||||||
|
if (hours > maxTime) maxTime = hours;
|
||||||
|
if (hours >= 50) {
|
||||||
|
eliteCount++;
|
||||||
|
} else if (hours < 5) {
|
||||||
|
inactiveCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// --- 2. Header (Slate-900 style) ---
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF0F172A), // slate-900
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
children: [
|
||||||
MyElevatedButton(
|
const Icon(Icons.local_taxi,
|
||||||
title: 'Giza',
|
color: Colors.yellow, size: 24),
|
||||||
onPressed: () {
|
const SizedBox(width: 8),
|
||||||
Get.to(() => DriverTheBestGiza());
|
Text(
|
||||||
}),
|
'Best Drivers Dashboard'.tr,
|
||||||
MyElevatedButton(
|
style: const TextStyle(
|
||||||
title: 'Alexandria',
|
color: Colors.white,
|
||||||
onPressed: () {
|
fontSize: 20,
|
||||||
Get.to(() => DriverTheBestAlexandria());
|
fontWeight: FontWeight.bold,
|
||||||
}),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
const SizedBox(height: 4),
|
||||||
height: Get.height * .7,
|
Row(
|
||||||
child: ListView.builder(
|
children: [
|
||||||
itemCount: driverthebest.driver.length,
|
const Icon(Icons.access_time,
|
||||||
itemBuilder: (context, index) {
|
color: Colors.grey, size: 12),
|
||||||
final driver = driverthebest.driver[index];
|
const SizedBox(width: 4),
|
||||||
return ListTile(
|
Text(
|
||||||
leading: CircleAvatar(
|
ctrl.lastUpdated.isNotEmpty
|
||||||
child: Text(
|
? 'Updated: ${ctrl.lastUpdated}'
|
||||||
(int.parse(driver['driver_count']) * 5 / 3600)
|
: 'Data Live',
|
||||||
.toStringAsFixed(
|
style: TextStyle(
|
||||||
0), // Perform division first, then convert to string
|
color: Colors.grey[400], fontSize: 12),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
title:
|
],
|
||||||
Text((driver['name_arabic']) ?? 'Unknown Name'),
|
),
|
||||||
subtitle:
|
// Action Buttons (Delete Box & Refresh)
|
||||||
Text('Phone: ${(driver['phone']) ?? 'N/A'}'),
|
Row(
|
||||||
trailing: IconButton(
|
children: [
|
||||||
onPressed: () async {
|
// Delete Box Icon
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title:
|
title: "Reset Paid Status",
|
||||||
'are you sure to pay to this driver gift'
|
middleText:
|
||||||
.tr,
|
"Are you sure you want to clear the list of paid drivers? This cannot be undone.",
|
||||||
middleText: '',
|
textConfirm: "Yes, Clear",
|
||||||
onConfirm: () async {
|
textCancel: "Cancel",
|
||||||
final wallet =
|
confirmTextColor: Colors.white,
|
||||||
Get.put(WalletController());
|
buttonColor: Colors.red,
|
||||||
await wallet.addPaymentToDriver(
|
onConfirm: () {
|
||||||
'200',
|
ctrl.clearPaidStorage();
|
||||||
driver['id'].toString(),
|
|
||||||
driver['token']);
|
|
||||||
await wallet.addSeferWallet(
|
|
||||||
'200', driver['id'].toString());
|
|
||||||
await CRUD().post(
|
|
||||||
link: AppLink.deleteRecord,
|
|
||||||
payload: {
|
|
||||||
'driver_id': driver['id'].toString()
|
|
||||||
});
|
|
||||||
driverthebest.driver.removeAt(index);
|
|
||||||
driverthebest.update();
|
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
onCancel: () => Get.back());
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.wallet_giftcard_rounded),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
icon: const Icon(Icons.delete_forever,
|
||||||
|
color: Colors.redAccent),
|
||||||
|
tooltip: "Clear Paid Storage",
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white10),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ctrl.fetchData();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh,
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// --- 3. Statistics Cards Grid ---
|
||||||
|
SizedBox(
|
||||||
|
height: 100, // Fixed height for cards
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard('Total',
|
||||||
|
totalDrivers.toString(), Colors.blue)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard('Elite',
|
||||||
|
eliteCount.toString(), Colors.amber)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard('Inactive',
|
||||||
|
inactiveCount.toString(), Colors.red)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
'Max Time',
|
||||||
|
'${maxTime.toStringAsFixed(1)}h',
|
||||||
|
Colors.green)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// --- 4. Search Bar ---
|
||||||
|
TextField(
|
||||||
|
onChanged: (val) => ctrl.updateSearchQuery(val),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search by phone number...',
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.search, color: Colors.grey),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// --- 5. Driver List ---
|
||||||
|
if (filteredDrivers.isEmpty)
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Text(
|
||||||
|
ctrl.searchQuery.isNotEmpty
|
||||||
|
? "No drivers found with this number"
|
||||||
|
: "No drivers available",
|
||||||
|
style: TextStyle(color: Colors.grey[400])),
|
||||||
|
))
|
||||||
|
else
|
||||||
|
ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: filteredDrivers.length,
|
||||||
|
separatorBuilder: (c, i) =>
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final driver = filteredDrivers[index];
|
||||||
|
return _buildDriverCard(
|
||||||
|
context, driver, index, ctrl);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper Methods ---
|
||||||
|
|
||||||
|
// Updated to parse the Arabic string format "5 ساعة 30 دقيقة"
|
||||||
|
double _calculateHoursFromStr(dynamic activeTimeStr) {
|
||||||
|
if (activeTimeStr == null || activeTimeStr is! String) return 0.0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int hours = 0;
|
||||||
|
int mins = 0;
|
||||||
|
|
||||||
|
// Extract hours
|
||||||
|
final hoursMatch = RegExp(r'(\d+)\s*ساعة').firstMatch(activeTimeStr);
|
||||||
|
if (hoursMatch != null) {
|
||||||
|
hours = int.parse(hoursMatch.group(1) ?? '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract minutes
|
||||||
|
final minsMatch = RegExp(r'(\d+)\s*دقيقة').firstMatch(activeTimeStr);
|
||||||
|
if (minsMatch != null) {
|
||||||
|
mins = int.parse(minsMatch.group(1) ?? '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return hours + (mins / 60.0);
|
||||||
|
} catch (e) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatCard(String title, String value, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border(right: BorderSide(color: color, width: 4)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
color: color.withOpacity(0.8),
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDriverCard(BuildContext context, Map driver, int index,
|
||||||
|
DriverCacheController controller) {
|
||||||
|
double hours = _calculateHoursFromStr(driver['active_time']);
|
||||||
|
String driverId = driver['id']?.toString() ?? 'null';
|
||||||
|
bool isPaid = controller.isDriverPaid(driverId);
|
||||||
|
|
||||||
|
// Determine Status Category (mimicking HTML logic)
|
||||||
|
String statusText;
|
||||||
|
Color statusColor;
|
||||||
|
if (hours >= 50) {
|
||||||
|
statusText = "Elite";
|
||||||
|
statusColor = Colors.amber;
|
||||||
|
} else if (hours >= 20) {
|
||||||
|
statusText = "Stable";
|
||||||
|
statusColor = Colors.green;
|
||||||
|
} else if (hours >= 5) {
|
||||||
|
statusText = "Experimental";
|
||||||
|
statusColor = Colors.blue;
|
||||||
|
} else {
|
||||||
|
statusText = "Inactive";
|
||||||
|
statusColor = Colors.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override colors if paid
|
||||||
|
Color cardBackground = isPaid ? Colors.teal.shade50 : Colors.white;
|
||||||
|
Color borderColor = isPaid ? Colors.teal : Colors.transparent;
|
||||||
|
|
||||||
|
// Calculate progress (max assumed 60 hours for 100% bar)
|
||||||
|
double progress = (hours / 60).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: cardBackground,
|
||||||
|
border: isPaid ? Border.all(color: borderColor, width: 2) : null,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (isPaid)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.teal,
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
child: const Text("PAID",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Avatar
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: statusColor.withOpacity(0.1),
|
||||||
|
radius: 24,
|
||||||
|
child: Text(
|
||||||
|
hours.toStringAsFixed(0),
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusColor, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Name and Phone
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
driver['name_arabic'] ?? 'Unknown Name',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: isPaid
|
||||||
|
? Colors.teal.shade900
|
||||||
|
: const Color(0xFF334155)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
driver['phone'] ?? 'N/A',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
driver['active_time'] ?? '',
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey[400]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status Badge
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: statusColor.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: statusColor,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Progress Bar
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("Performance",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey[600])),
|
||||||
|
Text("${hours.toStringAsFixed(2)} hrs",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.grey[100],
|
||||||
|
color: statusColor,
|
||||||
|
minHeight: 6,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Divider(height: 1),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Actions Row
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
// Pay Gift Button (The specific request)
|
||||||
|
isPaid
|
||||||
|
? const Text("Payment Completed",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.teal, fontWeight: FontWeight.bold))
|
||||||
|
: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
_showPayDialog(driver, controller);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.card_giftcard, size: 16),
|
||||||
|
label: Text("Pay Gift".tr),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.indigo, // Dark blue/purple
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: const Center(
|
|
||||||
child: Text('No drivers available.'),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
],
|
],
|
||||||
isleading: true,
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPayDialog(Map driver, DriverCacheController controller) {
|
||||||
|
// Check for valid ID immediately
|
||||||
|
String driverId = driver['driver_id']?.toString() ?? '';
|
||||||
|
String phone = driver['phone']?.toString() ?? '';
|
||||||
|
if (driverId.isEmpty || driverId == 'null') {
|
||||||
|
Get.snackbar("Error", "Cannot pay driver with missing ID",
|
||||||
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller for the Amount Field
|
||||||
|
final TextEditingController amountController =
|
||||||
|
TextEditingController(text: '50000');
|
||||||
|
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: 'Confirm Payment',
|
||||||
|
titleStyle: const TextStyle(
|
||||||
|
color: Color(0xFF0F172A), fontWeight: FontWeight.bold),
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.wallet_giftcard, size: 50, color: Colors.indigo),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Sending gift to ${driver['name_arabic']}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
// Amount Field
|
||||||
|
TextFormField(
|
||||||
|
controller: amountController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Amount (SYP)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.attach_money),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
textConfirm: 'Pay Now',
|
||||||
|
confirmTextColor: Colors.white,
|
||||||
|
buttonColor: Colors.indigo,
|
||||||
|
onConfirm: () async {
|
||||||
|
final wallet = Get.put(WalletController());
|
||||||
|
|
||||||
|
// Get amount from field
|
||||||
|
String amount = amountController.text.trim();
|
||||||
|
if (amount.isEmpty) amount = '0';
|
||||||
|
|
||||||
|
// String driverToken = driver['token'] ?? '';
|
||||||
|
|
||||||
|
// 1. Add Payment
|
||||||
|
await wallet.addDriverWallet('gift_connect', driverId, amount, phone);
|
||||||
|
|
||||||
|
// 2. Add to Sefer Wallet
|
||||||
|
//await wallet.addSeferWallet(amount, driverId);
|
||||||
|
|
||||||
|
// 3. Delete Record via CRUD
|
||||||
|
// await CRUD()
|
||||||
|
// .post(link: AppLink.deleteRecord, payload: {'driver_id': driverId});
|
||||||
|
|
||||||
|
// 4. UI Update & Storage
|
||||||
|
// Mark as paid instead of removing completely, so we can see the color change
|
||||||
|
controller.markAsPaid(driverId);
|
||||||
|
|
||||||
|
Get.back(); // Close Dialog
|
||||||
|
|
||||||
|
Get.snackbar("Success", "Payment of $amount EGP sent to driver",
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
},
|
||||||
|
textCancel: 'Cancel',
|
||||||
|
onCancel: () => Get.back(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
448
lib/views/admin/drivers/driver_tracker_screen.dart
Normal file
448
lib/views/admin/drivers/driver_tracker_screen.dart
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:url_launcher/url_launcher.dart'; // ضروري من أجل الاتصال
|
||||||
|
|
||||||
|
import '../../../constant/box_name.dart';
|
||||||
|
import '../../../main.dart';
|
||||||
|
|
||||||
|
class IntaleqTrackerScreen extends StatefulWidget {
|
||||||
|
const IntaleqTrackerScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<IntaleqTrackerScreen> createState() => _IntaleqTrackerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IntaleqTrackerScreenState extends State<IntaleqTrackerScreen> {
|
||||||
|
// === Map Controller ===
|
||||||
|
final MapController _mapController = MapController();
|
||||||
|
List<Marker> _markers = [];
|
||||||
|
|
||||||
|
// === State Variables ===
|
||||||
|
bool isLiveMode = true;
|
||||||
|
bool isLoading = false;
|
||||||
|
String lastUpdated = "جاري التحميل...";
|
||||||
|
|
||||||
|
// === Counters ===
|
||||||
|
int liveCount = 0;
|
||||||
|
int dayCount = 0;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
// === Admin Info ===
|
||||||
|
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||||
|
bool get isSuperAdmin =>
|
||||||
|
myPhone == '963942542053' || myPhone == '963992952235';
|
||||||
|
|
||||||
|
// === URLs ===
|
||||||
|
final String _baseDir = "https://api.intaleq.xyz/intaleq/ride/location/";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
// === تعديل 1: التحديث كل 5 دقائق بدلاً من 15 ثانية ===
|
||||||
|
_timer = Timer.periodic(const Duration(minutes: 5), (timer) {
|
||||||
|
if (mounted) fetchData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_mapController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === دالة إجراء الاتصال ===
|
||||||
|
Future<void> _makePhoneCall(String phoneNumber) async {
|
||||||
|
final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber);
|
||||||
|
if (await canLaunchUrl(launchUri)) {
|
||||||
|
await launchUrl(launchUri);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("لا يمكن إجراء الاتصال لهذا الرقم")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Fetch Data Function ===
|
||||||
|
Future<void> fetchData() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => isLoading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. طلب التحديث من PHP
|
||||||
|
String updateUrl =
|
||||||
|
"${_baseDir}getUpdatedLocationForAdmin.php?mode=${isLiveMode ? 'live' : 'day'}";
|
||||||
|
await http.get(Uri.parse(updateUrl));
|
||||||
|
|
||||||
|
String v = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
|
||||||
|
// === Live Data ===
|
||||||
|
final responseLive =
|
||||||
|
await http.get(Uri.parse("${_baseDir}locations_live.json?v=$v"));
|
||||||
|
if (responseLive.statusCode == 200) {
|
||||||
|
final data = json.decode(responseLive.body);
|
||||||
|
List drivers = (data is Map && data.containsKey('drivers'))
|
||||||
|
? data['drivers']
|
||||||
|
: data;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
liveCount = drivers.length;
|
||||||
|
if (isLiveMode) _buildMarkers(drivers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Day Data ===
|
||||||
|
final responseDay =
|
||||||
|
await http.get(Uri.parse("${_baseDir}locations_day.json?v=$v"));
|
||||||
|
if (responseDay.statusCode == 200) {
|
||||||
|
final data = json.decode(responseDay.body);
|
||||||
|
List drivers = (data is Map && data.containsKey('drivers'))
|
||||||
|
? data['drivers']
|
||||||
|
: data;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
dayCount = drivers.length;
|
||||||
|
if (!isLiveMode) _buildMarkers(drivers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
lastUpdated = DateTime.now().toString().substring(11, 19);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("Exception: $e");
|
||||||
|
setState(() => lastUpdated = "خطأ في الاتصال");
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Build Markers ===
|
||||||
|
void _buildMarkers(List<dynamic> drivers) {
|
||||||
|
List<Marker> newMarkers = [];
|
||||||
|
|
||||||
|
for (var d in drivers) {
|
||||||
|
double lat = double.tryParse((d['lat'] ?? "0").toString()) ?? 0.0;
|
||||||
|
double lon = double.tryParse((d['lon'] ?? "0").toString()) ?? 0.0;
|
||||||
|
double heading = double.tryParse((d['heading'] ?? "0").toString()) ?? 0.0;
|
||||||
|
|
||||||
|
String id = (d['id'] ?? "Unknown").toString();
|
||||||
|
String speed = (d['speed'] ?? "0").toString();
|
||||||
|
String name = (d['name'] ?? "كابتن").toString();
|
||||||
|
String phone = (d['phone'] ?? "").toString();
|
||||||
|
String completed = (d['completed'] ?? "0").toString();
|
||||||
|
String cancelled = (d['cancelled'] ?? "0").toString();
|
||||||
|
|
||||||
|
if (lat != 0 && lon != 0) {
|
||||||
|
newMarkers.add(
|
||||||
|
Marker(
|
||||||
|
point: LatLng(lat, lon),
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_showDriverInfoDialog(
|
||||||
|
driverId: id,
|
||||||
|
name: name,
|
||||||
|
phone: phone,
|
||||||
|
speed: speed,
|
||||||
|
heading: heading,
|
||||||
|
completed: completed,
|
||||||
|
cancelled: cancelled,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(blurRadius: 3, color: Colors.black26)
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
Transform.rotate(
|
||||||
|
angle: heading * (math.pi / 180),
|
||||||
|
child: Icon(
|
||||||
|
Icons.navigation,
|
||||||
|
color: isLiveMode
|
||||||
|
? const Color(0xFF27AE60)
|
||||||
|
: const Color(0xFF2980B9),
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_markers = newMarkers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Dialog Function ===
|
||||||
|
void _showDriverInfoDialog({
|
||||||
|
required String driverId,
|
||||||
|
required String name,
|
||||||
|
required String phone,
|
||||||
|
required String speed,
|
||||||
|
required double heading,
|
||||||
|
required String completed,
|
||||||
|
required String cancelled,
|
||||||
|
}) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text("بيانات الكابتن",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF2C3E50))),
|
||||||
|
const Divider(thickness: 1, height: 25),
|
||||||
|
_infoRow(Icons.person, "الاسم", name),
|
||||||
|
_infoRow(Icons.badge, "المعرف (ID)", driverId),
|
||||||
|
_infoRow(Icons.speed, "السرعة", "$speed كم/س"),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade200)),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_statItem("مكتملة", completed, Colors.green),
|
||||||
|
Container(
|
||||||
|
width: 1, height: 30, color: Colors.grey.shade300),
|
||||||
|
_statItem("ملغاة", cancelled, Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// === تعديل 2: جعل رقم الهاتف قابلاً للنقر ===
|
||||||
|
if (isSuperAdmin) ...[
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (phone.isNotEmpty) _makePhoneCall(phone);
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF3CD),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: const Color(0xFFFFEEBA))),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _infoRow(Icons.phone, "الهاتف", phone,
|
||||||
|
isPrivate: true)),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
const Icon(Icons.call, color: Colors.green, size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF2C3E50),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text("إغلاق"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Widgets
|
||||||
|
Widget _infoRow(IconData icon, String label, String value,
|
||||||
|
{bool isPrivate = false}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon,
|
||||||
|
size: 20,
|
||||||
|
color: isPrivate ? Colors.orange[800] : Colors.grey[600]),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text("$label: ",
|
||||||
|
style:
|
||||||
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
Expanded(
|
||||||
|
child: Text(value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight:
|
||||||
|
isPrivate ? FontWeight.bold : FontWeight.normal),
|
||||||
|
textAlign: TextAlign.end)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _statItem(String label, String val, Color color) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(val,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
Text(label, style: const TextStyle(fontSize: 11, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("نظام تتبع الكباتن"),
|
||||||
|
backgroundColor: const Color(0xFF2C3E50),
|
||||||
|
foregroundColor: Colors.white),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
FlutterMap(
|
||||||
|
mapController: _mapController,
|
||||||
|
options: const MapOptions(
|
||||||
|
initialCenter: LatLng(33.513, 36.276), initialZoom: 10.0),
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
userAgentPackageName: 'com.tripz.app'),
|
||||||
|
MarkerLayer(markers: _markers),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// === Dashboard ===
|
||||||
|
Positioned(
|
||||||
|
top: 20,
|
||||||
|
right: 15,
|
||||||
|
child: Container(
|
||||||
|
width: 260,
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.95),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(color: Colors.black12, blurRadius: 10)
|
||||||
|
]),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
const Text("لوحة التحكم",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
|
// أزرار التبديل
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _modeBtn("أرشيف اليوم", !isLiveMode, () {
|
||||||
|
setState(() => isLiveMode = false);
|
||||||
|
fetchData();
|
||||||
|
})),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _modeBtn("مباشر", isLiveMode, () {
|
||||||
|
setState(() => isLiveMode = true);
|
||||||
|
fetchData();
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// === عرض العدادين معاً ===
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("$liveCount",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF27AE60),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14)),
|
||||||
|
const Text("نشط الآن (مباشر):",
|
||||||
|
style: TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("$dayCount",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF2980B9),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14)),
|
||||||
|
const Text("إجمالي اليوم:",
|
||||||
|
style: TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(isLoading ? "جاري التحديث..." : "تحديث: $lastUpdated",
|
||||||
|
style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: isLoading ? null : fetchData,
|
||||||
|
child: const Text("تحديث البيانات")))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _modeBtn(String title, bool active, VoidCallback onTap) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: active ? const Color(0xFF3498DB) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: const Color(0xFF3498DB))),
|
||||||
|
child: Text(title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: active ? Colors.white : const Color(0xFF3498DB),
|
||||||
|
fontWeight: active ? FontWeight.bold : FontWeight.normal)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
557
lib/views/admin/drivers/monitor_ride.dart
Normal file
557
lib/views/admin/drivers/monitor_ride.dart
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
// Keep your specific imports
|
||||||
|
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||||
|
import 'package:sefer_admin1/print.dart';
|
||||||
|
|
||||||
|
/// --------------------------------------------------------------------------
|
||||||
|
/// 1. DATA MODELS
|
||||||
|
/// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class DriverLocation {
|
||||||
|
final double latitude;
|
||||||
|
final double longitude;
|
||||||
|
final double speed;
|
||||||
|
final double heading;
|
||||||
|
final String updatedAt;
|
||||||
|
|
||||||
|
DriverLocation({
|
||||||
|
required this.latitude,
|
||||||
|
required this.longitude,
|
||||||
|
required this.speed,
|
||||||
|
required this.heading,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DriverLocation.fromJson(Map<String, dynamic> json) {
|
||||||
|
return DriverLocation(
|
||||||
|
latitude: double.tryParse(json['latitude'].toString()) ?? 0.0,
|
||||||
|
longitude: double.tryParse(json['longitude'].toString()) ?? 0.0,
|
||||||
|
speed: double.tryParse(json['speed'].toString()) ?? 0.0,
|
||||||
|
heading: double.tryParse(json['heading'].toString()) ?? 0.0,
|
||||||
|
updatedAt: json['updated_at'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// --------------------------------------------------------------------------
|
||||||
|
/// 2. GETX CONTROLLER
|
||||||
|
/// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class RideMonitorController extends GetxController {
|
||||||
|
// CONFIGURATION
|
||||||
|
final String apiUrl =
|
||||||
|
"https://api.intaleq.xyz/intaleq/Admin/rides/monitorRide.php";
|
||||||
|
|
||||||
|
// INPUT CONTROLLERS
|
||||||
|
final TextEditingController phoneInputController = TextEditingController();
|
||||||
|
|
||||||
|
// OBSERVABLES
|
||||||
|
var isTracking = false.obs;
|
||||||
|
var isLoading = false.obs;
|
||||||
|
var hasError = false.obs;
|
||||||
|
var errorMessage = ''.obs;
|
||||||
|
|
||||||
|
// Driver & Ride Data
|
||||||
|
var driverLocation = Rxn<DriverLocation>();
|
||||||
|
var driverName = "Unknown Driver".obs;
|
||||||
|
var rideStatus = "Waiting...".obs;
|
||||||
|
|
||||||
|
// Route Data
|
||||||
|
var startPoint = Rxn<LatLng>();
|
||||||
|
var endPoint = Rxn<LatLng>();
|
||||||
|
var routePolyline = <LatLng>[].obs; // List of points for the line
|
||||||
|
|
||||||
|
// Map Variables
|
||||||
|
final MapController mapController = MapController();
|
||||||
|
Timer? _timer;
|
||||||
|
bool _isFirstLoad = true; // To trigger auto-fit bounds only on first success
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
phoneInputController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ACTIONS ---
|
||||||
|
|
||||||
|
void startSearch() {
|
||||||
|
if (phoneInputController.text.trim().isEmpty) {
|
||||||
|
Get.snackbar("Error", "Please enter a phone number");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
hasError.value = false;
|
||||||
|
errorMessage.value = '';
|
||||||
|
driverLocation.value = null;
|
||||||
|
startPoint.value = null;
|
||||||
|
endPoint.value = null;
|
||||||
|
routePolyline.clear();
|
||||||
|
driverName.value = "Loading...";
|
||||||
|
rideStatus.value = "Loading...";
|
||||||
|
_isFirstLoad = true;
|
||||||
|
|
||||||
|
// Switch UI
|
||||||
|
isTracking.value = true;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Start fetching
|
||||||
|
fetchRideData();
|
||||||
|
|
||||||
|
// Start Polling
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
||||||
|
fetchRideData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopTracking() {
|
||||||
|
_timer?.cancel();
|
||||||
|
isTracking.value = false;
|
||||||
|
isLoading.value = false;
|
||||||
|
phoneInputController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchRideData() async {
|
||||||
|
final phone = phoneInputController.text.trim();
|
||||||
|
if (phone.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await CRUD().post(
|
||||||
|
link: apiUrl,
|
||||||
|
payload: {"phone": phone},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log.print('response: ${response}');
|
||||||
|
|
||||||
|
if (response != 'failure') {
|
||||||
|
final jsonResponse = response;
|
||||||
|
|
||||||
|
if ((jsonResponse['message'] != null &&
|
||||||
|
jsonResponse['message'] != 'failure') ||
|
||||||
|
jsonResponse['status'] == 'success') {
|
||||||
|
final data =
|
||||||
|
jsonResponse['message'] ?? jsonResponse['data'] ?? jsonResponse;
|
||||||
|
|
||||||
|
// 1. Parse Driver Info
|
||||||
|
if (data['driver_details'] != null) {
|
||||||
|
driverName.value = data['driver_details']['fullname'] ?? "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse Ride Info & Route
|
||||||
|
if (data['ride_details'] != null) {
|
||||||
|
rideStatus.value = data['ride_details']['status'] ?? "Unknown";
|
||||||
|
|
||||||
|
// Parse Start/End Locations (Format: "lat,lng")
|
||||||
|
String? startStr = data['ride_details']['start_location'];
|
||||||
|
String? endStr = data['ride_details']['end_location'];
|
||||||
|
|
||||||
|
LatLng? s = _parseLatLngString(startStr);
|
||||||
|
LatLng? e = _parseLatLngString(endStr);
|
||||||
|
|
||||||
|
if (s != null && e != null) {
|
||||||
|
startPoint.value = s;
|
||||||
|
endPoint.value = e;
|
||||||
|
routePolyline.value = [s, e]; // Straight line for now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Parse Live Location
|
||||||
|
final locData = data['driver_location'];
|
||||||
|
if (locData is Map<String, dynamic>) {
|
||||||
|
final newLocation = DriverLocation.fromJson(locData);
|
||||||
|
driverLocation.value = newLocation;
|
||||||
|
|
||||||
|
// 4. Update Camera Bounds
|
||||||
|
_updateMapBounds();
|
||||||
|
} else {
|
||||||
|
// Even if no live driver, we might want to show the route
|
||||||
|
if (startPoint.value != null && endPoint.value != null) {
|
||||||
|
_updateMapBounds();
|
||||||
|
}
|
||||||
|
print("No live location coordinates.");
|
||||||
|
}
|
||||||
|
|
||||||
|
hasError.value = false;
|
||||||
|
} else {
|
||||||
|
hasError.value = true;
|
||||||
|
errorMessage.value = jsonResponse['message'] ??
|
||||||
|
"Phone number not found or no active ride.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasError.value = true;
|
||||||
|
errorMessage.value = "Connection Failed";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Polling Error: $e");
|
||||||
|
if (isLoading.value) {
|
||||||
|
hasError.value = true;
|
||||||
|
errorMessage.value = e.toString();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to parse "lat,lng" string
|
||||||
|
LatLng? _parseLatLngString(String? str) {
|
||||||
|
if (str == null || !str.contains(',')) return null;
|
||||||
|
try {
|
||||||
|
final parts = str.split(',');
|
||||||
|
final lat = double.parse(parts[0].trim());
|
||||||
|
final lng = double.parse(parts[1].trim());
|
||||||
|
return LatLng(lat, lng);
|
||||||
|
} catch (e) {
|
||||||
|
print("Error parsing location string '$str': $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic to fit start, end, and driver on screen
|
||||||
|
void _updateMapBounds() {
|
||||||
|
// Only auto-fit on the first successful load to avoid fighting user pan/zoom
|
||||||
|
if (!_isFirstLoad) return;
|
||||||
|
|
||||||
|
List<LatLng> pointsToFit = [];
|
||||||
|
|
||||||
|
if (startPoint.value != null) pointsToFit.add(startPoint.value!);
|
||||||
|
if (endPoint.value != null) pointsToFit.add(endPoint.value!);
|
||||||
|
if (driverLocation.value != null) {
|
||||||
|
pointsToFit.add(LatLng(
|
||||||
|
driverLocation.value!.latitude, driverLocation.value!.longitude));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointsToFit.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final bounds = LatLngBounds.fromPoints(pointsToFit);
|
||||||
|
mapController.fitCamera(
|
||||||
|
CameraFit.bounds(
|
||||||
|
bounds: bounds,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.all(80.0), // Padding so markers aren't on edge
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_isFirstLoad = false; // Disable auto-fit after initial success
|
||||||
|
} catch (e) {
|
||||||
|
print("Map Controller not ready yet: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// --------------------------------------------------------------------------
|
||||||
|
/// 3. UI SCREEN
|
||||||
|
/// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class RideMonitorScreen extends StatelessWidget {
|
||||||
|
const RideMonitorScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final RideMonitorController controller = Get.put(RideMonitorController());
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Admin Ride Monitor"),
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
actions: [
|
||||||
|
Obx(() => controller.isTracking.value
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: controller.stopTracking,
|
||||||
|
tooltip: "Stop Tracking",
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Obx(() {
|
||||||
|
if (!controller.isTracking.value) {
|
||||||
|
return _buildSearchForm(context, controller);
|
||||||
|
}
|
||||||
|
return _buildMapTrackingView(controller);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchForm(
|
||||||
|
BuildContext context, RideMonitorController controller) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.map_outlined, size: 80, color: Colors.blueAccent),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
"Track Active Ride",
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
"Enter Driver or Passenger Phone Number",
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
TextField(
|
||||||
|
controller: controller.phoneInputController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Phone Number",
|
||||||
|
hintText: "e.g. 9639...",
|
||||||
|
border:
|
||||||
|
OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
prefixIcon: const Icon(Icons.phone),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: controller.startSearch,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: const Text("Start Monitoring",
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMapTrackingView(RideMonitorController controller) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
FlutterMap(
|
||||||
|
mapController: controller.mapController,
|
||||||
|
options: MapOptions(
|
||||||
|
initialCenter: const LatLng(30.0444, 31.2357),
|
||||||
|
initialZoom: 12.0,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
userAgentPackageName: 'com.sefer.admin',
|
||||||
|
),
|
||||||
|
|
||||||
|
// 1. ROUTE LINE (Polyline)
|
||||||
|
if (controller.routePolyline.isNotEmpty)
|
||||||
|
PolylineLayer(
|
||||||
|
polylines: [
|
||||||
|
Polyline(
|
||||||
|
points: controller.routePolyline.value,
|
||||||
|
strokeWidth: 5.0,
|
||||||
|
color: Colors.blueAccent.withOpacity(0.8),
|
||||||
|
borderStrokeWidth: 2.0,
|
||||||
|
borderColor: Colors.blue[900]!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// 2. START & END MARKERS
|
||||||
|
MarkerLayer(
|
||||||
|
markers: [
|
||||||
|
// Start Point (Green Flag)
|
||||||
|
if (controller.startPoint.value != null)
|
||||||
|
Marker(
|
||||||
|
point: controller.startPoint.value!,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child:
|
||||||
|
const Icon(Icons.flag, color: Colors.green, size: 40),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
),
|
||||||
|
|
||||||
|
// End Point (Red Flag)
|
||||||
|
if (controller.endPoint.value != null)
|
||||||
|
Marker(
|
||||||
|
point: controller.endPoint.value!,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: const Icon(Icons.flag, color: Colors.red, size: 40),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Driver Car Marker
|
||||||
|
if (controller.driverLocation.value != null)
|
||||||
|
Marker(
|
||||||
|
point: LatLng(
|
||||||
|
controller.driverLocation.value!.latitude,
|
||||||
|
controller.driverLocation.value!.longitude,
|
||||||
|
),
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: (controller.driverLocation.value!.heading *
|
||||||
|
(3.14159 / 180)),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.directions_car_filled,
|
||||||
|
color: Colors
|
||||||
|
.black, // Dark car for visibility on blue line
|
||||||
|
size: 35,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"${controller.driverLocation.value!.speed.toInt()} km",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 10, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// LOADING OVERLAY
|
||||||
|
if (controller.isLoading.value &&
|
||||||
|
controller.driverLocation.value == null &&
|
||||||
|
controller.startPoint.value == null)
|
||||||
|
Container(
|
||||||
|
color: Colors.black45,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white)),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ERROR OVERLAY
|
||||||
|
if (controller.hasError.value)
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.all(20),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(blurRadius: 10, color: Colors.black26)
|
||||||
|
]),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline, color: Colors.red, size: 40),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(controller.errorMessage.value,
|
||||||
|
textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: controller.stopTracking,
|
||||||
|
child: const Text("Back"))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// INFO CARD
|
||||||
|
if (!controller.hasError.value && !controller.isLoading.value)
|
||||||
|
Positioned(
|
||||||
|
bottom: 20,
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
child: Card(
|
||||||
|
elevation: 8,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const CircleAvatar(
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
child: Icon(Icons.person, color: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
controller.driverName.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle,
|
||||||
|
size: 10,
|
||||||
|
color:
|
||||||
|
controller.rideStatus.value == 'Begin'
|
||||||
|
? Colors.green
|
||||||
|
: Colors.grey),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(controller.rideStatus.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (controller.driverLocation.value != null)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildInfoBadge(Icons.speed,
|
||||||
|
"${controller.driverLocation.value!.speed.toStringAsFixed(1)} km/h"),
|
||||||
|
_buildInfoBadge(
|
||||||
|
Icons.access_time,
|
||||||
|
controller.driverLocation.value!.updatedAt
|
||||||
|
.split(' ')
|
||||||
|
.last),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Text("Connecting to driver...",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.orange,
|
||||||
|
fontStyle: FontStyle.italic)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoBadge(IconData icon, String text) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[800], fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
352
lib/views/admin/error/error/error_page.dart
Normal file
352
lib/views/admin/error/error/error_page.dart
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sefer_admin1/constant/colors.dart';
|
||||||
|
import 'package:sefer_admin1/constant/links.dart';
|
||||||
|
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||||
|
|
||||||
|
class ErrorLog {
|
||||||
|
final String id;
|
||||||
|
final String error;
|
||||||
|
final String userId;
|
||||||
|
final String userType;
|
||||||
|
final String phone;
|
||||||
|
final String createdAt;
|
||||||
|
final String device;
|
||||||
|
final String details;
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
ErrorLog({
|
||||||
|
required this.id,
|
||||||
|
required this.error,
|
||||||
|
required this.userId,
|
||||||
|
required this.userType,
|
||||||
|
required this.phone,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.device,
|
||||||
|
required this.details,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ErrorLog.fromJson(Map<String, dynamic> j) => ErrorLog(
|
||||||
|
id: (j['id'] ?? '').toString(),
|
||||||
|
error: (j['error'] ?? '').toString(),
|
||||||
|
userId: (j['userId'] ?? '').toString(),
|
||||||
|
userType: (j['userType'] ?? '').toString(),
|
||||||
|
phone: (j['phone'] ?? '').toString(),
|
||||||
|
createdAt: (j['created_at'] ?? '').toString(),
|
||||||
|
device: (j['device'] ?? '').toString(),
|
||||||
|
details: (j['details'] ?? '').toString(),
|
||||||
|
status: (j['status'] ?? '').toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorListPage extends StatefulWidget {
|
||||||
|
const ErrorListPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ErrorListPage> createState() => _ErrorListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorListPageState extends State<ErrorListPage> {
|
||||||
|
static String baseUrl = '${AppLink.server}/Admin/error';
|
||||||
|
static const String listEndpoint = "error_list_last20.php";
|
||||||
|
static const String searchEndpoint = "error_search_by_phone.php";
|
||||||
|
|
||||||
|
final TextEditingController _phoneCtrl = TextEditingController();
|
||||||
|
bool _loading = false;
|
||||||
|
String? _errorMsg;
|
||||||
|
List<ErrorLog> _items = [];
|
||||||
|
bool _searchMode = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchLast20();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchLast20() async {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_errorMsg = null;
|
||||||
|
_searchMode = false;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final res =
|
||||||
|
await CRUD().post(link: "$baseUrl/$listEndpoint", payload: {});
|
||||||
|
final map = (res);
|
||||||
|
if (map['status'] == 'success') {
|
||||||
|
final List data = (map['message'] ?? []) as List;
|
||||||
|
final items = data.map((e) => ErrorLog.fromJson(e)).toList();
|
||||||
|
setState(() {
|
||||||
|
_items = items.cast<ErrorLog>();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() => _errorMsg = map['message']?.toString() ?? 'Failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _errorMsg = e.toString());
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _searchByPhone() async {
|
||||||
|
final phone = _phoneCtrl.text.trim();
|
||||||
|
if (phone.isEmpty) {
|
||||||
|
return _fetchLast20();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_errorMsg = null;
|
||||||
|
_searchMode = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final res = await CRUD()
|
||||||
|
.post(link: "$baseUrl/$searchEndpoint", payload: {"phone": phone});
|
||||||
|
final map = (res);
|
||||||
|
if (map['status'] == 'success') {
|
||||||
|
final List data = (map['message'] ?? []) as List;
|
||||||
|
final items = data.map((e) => ErrorLog.fromJson(e)).toList();
|
||||||
|
setState(() {
|
||||||
|
_items = items.cast<ErrorLog>();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() => _errorMsg = map['message']?.toString() ?? 'Failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _errorMsg = e.toString());
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearSearch() {
|
||||||
|
_phoneCtrl.clear();
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
_fetchLast20();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey[100],
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('سجل الأخطاء'),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
elevation: 1.0,
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_SearchBar(
|
||||||
|
controller: _phoneCtrl,
|
||||||
|
onSearch: _searchByPhone,
|
||||||
|
onClear: _clearSearch,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _buildBody(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
|
if (_loading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMsg != null) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
_errorMsg!,
|
||||||
|
style: TextStyle(color: Colors.red[700]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_items.isEmpty) {
|
||||||
|
return const Center(child: Text('لا توجد سجلات'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
if (_searchMode) {
|
||||||
|
await _searchByPhone();
|
||||||
|
} else {
|
||||||
|
await _fetchLast20();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
itemCount: _items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final e = _items[index];
|
||||||
|
return _ErrorTile(e);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchBar extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final VoidCallback onSearch;
|
||||||
|
final VoidCallback onClear;
|
||||||
|
|
||||||
|
const _SearchBar({
|
||||||
|
required this.controller,
|
||||||
|
required this.onSearch,
|
||||||
|
required this.onClear,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
onSubmitted: (_) => onSearch(),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'بحث برقم الهاتف...',
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: onSearch,
|
||||||
|
child: const Text('بحث'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: onClear,
|
||||||
|
child: const Text('مسح'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ErrorTile extends StatelessWidget {
|
||||||
|
final ErrorLog item;
|
||||||
|
const _ErrorTile(this.item);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
// تحديد الألوان بناءً على نوع المستخدم
|
||||||
|
Color? typeBgColor;
|
||||||
|
Color? typeTextColor;
|
||||||
|
|
||||||
|
final type = item.userType.toLowerCase();
|
||||||
|
if (type.contains('driver') || type.contains('سائق')) {
|
||||||
|
typeBgColor = Colors.green.shade100;
|
||||||
|
typeTextColor = Colors.green.shade800;
|
||||||
|
} else if (type.contains('passenger') || type.contains('راكب')) {
|
||||||
|
typeBgColor = Colors.amber.shade100; // لون ذهبي/أصفر
|
||||||
|
typeTextColor = Colors.amber.shade900;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// السطر الأول: نص الخطأ (قابل للنسخ)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SelectableText(
|
||||||
|
item.error.isEmpty ? '(بدون عنوان)' : item.error,
|
||||||
|
style: theme.textTheme.titleMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// تم إزالة _StatusPill (ويدجت الحالة New) من هنا
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// تفاصيل مختصرة (قابل للنسخ)
|
||||||
|
if (item.details.isNotEmpty)
|
||||||
|
SelectableText(
|
||||||
|
item.details,
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// معلومات تقنية
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
_KV('الهاتف', item.phone),
|
||||||
|
_KV('المستخدم', item.userId),
|
||||||
|
// تمرير الألوان المخصصة لنوع المستخدم
|
||||||
|
_KV(
|
||||||
|
'النوع',
|
||||||
|
item.userType,
|
||||||
|
bgColor: typeBgColor,
|
||||||
|
textColor: typeTextColor,
|
||||||
|
),
|
||||||
|
_KV('الجهاز', item.device),
|
||||||
|
_KV('التاريخ', item.createdAt),
|
||||||
|
_KV('ID', item.id),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KV extends StatelessWidget {
|
||||||
|
final String k;
|
||||||
|
final String v;
|
||||||
|
final Color? bgColor;
|
||||||
|
final Color? textColor;
|
||||||
|
|
||||||
|
const _KV(this.k, this.v, {this.bgColor, this.textColor});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor ?? Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text.rich(TextSpan(
|
||||||
|
style: TextStyle(fontSize: 11, color: textColor ?? Colors.grey[600]),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: "$k: "),
|
||||||
|
TextSpan(
|
||||||
|
text: v.isEmpty ? '—' : v,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: textColor ?? Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,205 +1,390 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_admin1/controller/functions/encrypt_decrypt.dart';
|
|
||||||
|
|
||||||
|
import '../../../constant/box_name.dart';
|
||||||
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/admin/passenger_admin_controller.dart';
|
import '../../../controller/admin/passenger_admin_controller.dart';
|
||||||
|
import '../../../main.dart'; // للوصول إلى box
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
import '../../widgets/my_scafold.dart';
|
import '../../widgets/my_scafold.dart';
|
||||||
import '../../widgets/my_textField.dart';
|
import '../../widgets/my_textField.dart';
|
||||||
import '../../widgets/mycircular.dart';
|
import '../../widgets/mycircular.dart';
|
||||||
import 'form_passenger.dart';
|
|
||||||
import 'passenger_details_page.dart';
|
import 'passenger_details_page.dart';
|
||||||
|
|
||||||
class Passengrs extends StatelessWidget {
|
class Passengrs extends StatelessWidget {
|
||||||
Passengrs({super.key});
|
Passengrs({super.key});
|
||||||
|
|
||||||
final PassengerAdminController passengerAdminController =
|
final PassengerAdminController passengerAdminController =
|
||||||
Get.put(PassengerAdminController());
|
Get.put(PassengerAdminController());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// 1. منطق السوبر أدمن
|
||||||
|
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||||
|
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||||
|
|
||||||
return MyScafolld(
|
return MyScafolld(
|
||||||
title: 'Passengrs'.tr,
|
title: 'Passengers Management'.tr,
|
||||||
isleading: true,
|
isleading: true,
|
||||||
body: [
|
body: [
|
||||||
GetBuilder<PassengerAdminController>(
|
// استخدام Expanded أو Container بطول الشاشة لتجنب المشاكل
|
||||||
builder: (passengerAdminController) => Column(
|
SizedBox(
|
||||||
children: [
|
height: Get.height, // تأمين مساحة العمل
|
||||||
passengerAdminController.isLoading
|
child: GetBuilder<PassengerAdminController>(
|
||||||
? const MyCircularProgressIndicator()
|
builder: (controller) {
|
||||||
: Column(
|
if (controller.isLoading) {
|
||||||
|
return const Center(child: MyCircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
// --- قسم الإحصائيات والجوائز (Dashboard) ---
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(5),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: _buildDashboardCard(context, controller),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- عنوان القائمة ---
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"All Passengers".tr,
|
||||||
|
style: AppStyle.title.copyWith(
|
||||||
|
fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${controller.passengersData['message']?.length ?? 0} Users",
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// --- قائمة الركاب ---
|
||||||
|
// استخدام Expanded هنا هو الحل الجذري لمكلة Overflow
|
||||||
|
Expanded(
|
||||||
|
child: _buildPassengersList(controller, isSuperAdmin),
|
||||||
|
),
|
||||||
|
|
||||||
|
// مساحة سفلية صغيرة لضمان عدم التصاق القائمة بالحافة
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- تصميم بطاقة الإحصائيات (Dashboard) ---
|
||||||
|
Widget _buildDashboardCard(
|
||||||
|
BuildContext context, PassengerAdminController controller) {
|
||||||
|
// جلب العدد بأمان
|
||||||
|
final String countValue = (controller.passengersData['message'] != null &&
|
||||||
|
controller.passengersData['message'].isNotEmpty)
|
||||||
|
? controller.passengersData['message'][0]['countPassenger']
|
||||||
|
?.toString() ??
|
||||||
|
'0'
|
||||||
|
: '0';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primaryColor.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.groups_rounded,
|
||||||
|
color: AppColor.primaryColor, size: 30),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Total Passengers'.tr,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
countValue,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 45,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.card_giftcard,
|
||||||
|
color: Colors.white, size: 20),
|
||||||
|
label: Text('Add Prize to Gold Passengers'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white, fontWeight: FontWeight.bold)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColor.yellowColor, // لون ذهبي
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_showAddPrizeDialog(controller);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- بناء قائمة الركاب ---
|
||||||
|
Widget _buildPassengersList(
|
||||||
|
PassengerAdminController controller, bool isSuperAdmin) {
|
||||||
|
final List<dynamic> passengers = controller.passengersData['message'] ?? [];
|
||||||
|
|
||||||
|
if (passengers.isEmpty) {
|
||||||
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
passengerAdmin(
|
Icon(Icons.person_off_outlined, size: 60, color: Colors.grey[300]),
|
||||||
passengerAdminController,
|
const SizedBox(height: 10),
|
||||||
'Passengers Count',
|
Text("No passengers found".tr,
|
||||||
'countPassenger',
|
style: TextStyle(color: Colors.grey[400])),
|
||||||
),
|
|
||||||
MyElevatedButton(
|
|
||||||
title: 'Add Prize to Gold Passengers',
|
|
||||||
onPressed: () {
|
|
||||||
var date = DateTime.now();
|
|
||||||
var day = date.weekday;
|
|
||||||
|
|
||||||
if (day == 6) {
|
|
||||||
// Saturday is 6
|
|
||||||
Get.defaultDialog(
|
|
||||||
title:
|
|
||||||
'Add Prize to Gold Passengers',
|
|
||||||
titleStyle: AppStyle.title,
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Add Points to their wallet as prize'
|
|
||||||
.tr,
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Form(
|
|
||||||
key:
|
|
||||||
passengerAdminController
|
|
||||||
.formPrizeKey,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller:
|
|
||||||
passengerAdminController
|
|
||||||
.passengerPrizeController,
|
|
||||||
label:
|
|
||||||
'Count of prize'
|
|
||||||
.tr,
|
|
||||||
hint: 'Count of prize'
|
|
||||||
.tr,
|
|
||||||
type: TextInputType
|
|
||||||
.number))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'Add',
|
|
||||||
onPressed: () async {
|
|
||||||
if (passengerAdminController
|
|
||||||
.formPrizeKey
|
|
||||||
.currentState!
|
|
||||||
.validate()) {
|
|
||||||
passengerAdminController
|
|
||||||
.addPassengerPrizeToWalletSecure();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
Get.defaultDialog(
|
|
||||||
title:
|
|
||||||
'This day is not allowed',
|
|
||||||
titleStyle: AppStyle.title,
|
|
||||||
middleText:
|
|
||||||
'Saturday only Allowed day',
|
|
||||||
middleTextStyle: AppStyle.title,
|
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'Ok'.tr,
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
],
|
return ListView.separated(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
physics: const BouncingScrollPhysics(),
|
||||||
const SizedBox(
|
itemCount: passengers.length,
|
||||||
height: 10,
|
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||||
),
|
|
||||||
formSearchPassengers(),
|
|
||||||
SizedBox(
|
|
||||||
height: Get.height * .5,
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: passengerAdminController
|
|
||||||
.passengersData['message'].length,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final user = passengerAdminController
|
final user = passengers[index];
|
||||||
.passengersData['message'][index];
|
return _buildPassengerItem(user, isSuperAdmin);
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
|
||||||
Get.to(const PassengerDetailsPage(),
|
|
||||||
arguments: {
|
|
||||||
'data': user,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(3),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(width: 2)),
|
|
||||||
child: ListTile(
|
|
||||||
title: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment
|
|
||||||
.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Name : ${(user['first_name'])} ${(user['last_name'])}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Rating : ${user['ratingPassenger']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment
|
|
||||||
.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Count Trip : ${user['countPassengerRide']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Count Driver Rate : ${user['countDriverRate']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
))
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Container passengerAdmin(PassengerAdminController passengerAdminController,
|
// --- عنصر الراكب الواحد (Card) ---
|
||||||
String title, String jsonField) {
|
Widget _buildPassengerItem(dynamic user, bool isSuperAdmin) {
|
||||||
|
String firstName = user['first_name'] ?? '';
|
||||||
|
String lastName = user['last_name'] ?? '';
|
||||||
|
String fullName = '$firstName $lastName'.trim();
|
||||||
|
if (fullName.isEmpty) fullName = 'Unknown User';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: Get.height * .1,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(border: Border.all(width: 2)),
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.05),
|
||||||
|
blurRadius: 5,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
onTap: () {
|
||||||
|
// الانتقال للتفاصيل مع تمرير صلاحية الأدمن
|
||||||
|
Get.to(
|
||||||
|
() => const PassengerDetailsPage(),
|
||||||
|
arguments: {'data': user, 'isSuperAdmin': isSuperAdmin},
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: GestureDetector(
|
child: Row(
|
||||||
onTap: () {},
|
children: [
|
||||||
|
// Avatar
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 25,
|
||||||
|
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||||
|
child: Text(
|
||||||
|
fullName.isNotEmpty ? fullName[0].toUpperCase() : 'U',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColor.primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
|
||||||
|
// Info
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
fullName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
// Stats Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.star_rounded,
|
||||||
|
size: 16, color: Colors.amber[700]),
|
||||||
|
Text(
|
||||||
|
" ${user['ratingPassenger'] ?? '0.0'} ",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(Icons.directions_car,
|
||||||
|
size: 14, color: Colors.grey[400]),
|
||||||
|
Text(
|
||||||
|
" ${user['countPassengerRide'] ?? '0'} Trips",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Phone Number (Masked logic)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.phone_iphone,
|
||||||
|
size: 12, color: Colors.grey[400]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_formatPhoneNumber(
|
||||||
|
user['phone'].toString(), isSuperAdmin),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Email (Show only if Super Admin)
|
||||||
|
if (isSuperAdmin && user['email'] != null) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
user['email'],
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 10, color: Colors.grey[400]),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Arrow
|
||||||
|
Icon(Icons.arrow_forward_ios_rounded,
|
||||||
|
size: 16, color: Colors.grey[300]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- دالة تنسيق الرقم (إظهار آخر 4 أرقام لغير الأدمن) ---
|
||||||
|
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
|
||||||
|
if (isSuperAdmin) return phone; // إظهار الرقم كاملاً للسوبر أدمن
|
||||||
|
|
||||||
|
// لغير الأدمن
|
||||||
|
if (phone.length <= 4) return phone;
|
||||||
|
String lastFour = phone.substring(phone.length - 4);
|
||||||
|
String masked = '*' * (phone.length - 4);
|
||||||
|
return '$masked$lastFour'; // النتيجة: *******5678
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- دالة إضافة الجوائز ---
|
||||||
|
void _showAddPrizeDialog(PassengerAdminController controller) {
|
||||||
|
// التحقق من يوم السبت
|
||||||
|
if (DateTime.now().weekday == DateTime.saturday) {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: 'Add Prize'.tr,
|
||||||
|
titleStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
contentPadding: const EdgeInsets.all(20),
|
||||||
|
content: Form(
|
||||||
|
key: controller.formPrizeKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title.tr,
|
'Add Points to Gold Passengers wallet'.tr,
|
||||||
style: AppStyle.title,
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(height: 20),
|
||||||
passengerAdminController.passengersData['message'][0][jsonField]
|
MyTextForm(
|
||||||
.toString(),
|
controller: controller.passengerPrizeController,
|
||||||
style: AppStyle.title,
|
label: 'Prize Amount'.tr,
|
||||||
|
hint: '1000...',
|
||||||
|
type: TextInputType.number,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
confirm: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: MyElevatedButton(
|
||||||
|
title: 'Add',
|
||||||
|
onPressed: () async {
|
||||||
|
if (controller.formPrizeKey.currentState!.validate()) {
|
||||||
|
controller.addPassengerPrizeToWalletSecure();
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
cancel: TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child:
|
||||||
|
Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Not Allowed'.tr,
|
||||||
|
'Prizes can only be added on Saturdays.'.tr,
|
||||||
|
backgroundColor: Colors.red.withOpacity(0.1),
|
||||||
|
colorText: Colors.red,
|
||||||
|
icon: const Icon(Icons.error_outline, color: Colors.red),
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
margin: const EdgeInsets.all(10),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,167 +1,458 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/style.dart';
|
|
||||||
import '../../../controller/admin/passenger_admin_controller.dart';
|
import '../../../controller/admin/passenger_admin_controller.dart';
|
||||||
|
import '../../../controller/functions/crud.dart';
|
||||||
import '../../../controller/firebase/firbase_messge.dart';
|
import '../../../controller/firebase/firbase_messge.dart';
|
||||||
|
import '../../../constant/links.dart';
|
||||||
|
import '../../../main.dart'; // To access 'box' for admin phone check
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
import '../../widgets/my_scafold.dart';
|
import '../../widgets/my_scafold.dart';
|
||||||
import '../../widgets/my_textField.dart';
|
import '../../widgets/my_textField.dart';
|
||||||
|
import 'form_passenger.dart';
|
||||||
|
|
||||||
class PassengerDetailsPage extends StatelessWidget {
|
class PassengerDetailsPage extends StatelessWidget {
|
||||||
const PassengerDetailsPage({super.key});
|
const PassengerDetailsPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final arguments = Get.arguments;
|
final Map<String, dynamic> data = Get.arguments['data'];
|
||||||
final Map<String, dynamic> data = arguments['data'];
|
final controller = Get.find<PassengerAdminController>();
|
||||||
var key = Get.find<PassengerAdminController>().formPrizeKey;
|
|
||||||
var titleNotify = Get.find<PassengerAdminController>().titleNotify;
|
// 1. Define Super Admin Logic (Same as Captains Page)
|
||||||
var bodyNotify = Get.find<PassengerAdminController>().bodyNotify;
|
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||||
|
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||||
|
|
||||||
return MyScafolld(
|
return MyScafolld(
|
||||||
title: data['first_name'],
|
title: 'Passenger Profile'.tr,
|
||||||
|
isleading: true,
|
||||||
body: [
|
body: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 40),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// --- Header Section (Avatar & Name) ---
|
||||||
|
_buildHeaderSection(context, data),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// --- Personal Information Card ---
|
||||||
|
_buildInfoCard(
|
||||||
|
title: 'Personal Information',
|
||||||
|
icon: Icons.person,
|
||||||
|
children: [
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.email_outlined,
|
||||||
|
'Email',
|
||||||
|
isSuperAdmin
|
||||||
|
? data['email']
|
||||||
|
: _maskEmail(data['email']),
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.phone_iphone,
|
||||||
|
'Phone',
|
||||||
|
_formatPhoneNumber(
|
||||||
|
data['phone'].toString(), isSuperAdmin),
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.transgender,
|
||||||
|
'Gender',
|
||||||
|
data['gender'] ?? 'Not specified',
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.cake_outlined,
|
||||||
|
'Birthdate',
|
||||||
|
data['birthdate'] ?? 'N/A',
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.location_on_outlined,
|
||||||
|
'Site',
|
||||||
|
data['site'] ?? 'N/A',
|
||||||
|
),
|
||||||
|
// SOS Phone is critical, usually shown, but we can mask it too if needed
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.sos,
|
||||||
|
'SOS Phone',
|
||||||
|
data['sosPhone'] ?? 'N/A',
|
||||||
|
valueColor: Colors.redAccent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// --- Ride Statistics Card ---
|
||||||
|
_buildInfoCard(
|
||||||
|
title: 'Activity & Stats',
|
||||||
|
icon: Icons.bar_chart_rounded,
|
||||||
|
children: [
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.star_rate_rounded,
|
||||||
|
'Rating',
|
||||||
|
'${data['ratingPassenger'] ?? 0.0}',
|
||||||
|
valueColor: Colors.amber[700],
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.directions_car_filled_outlined,
|
||||||
|
'Total Rides',
|
||||||
|
data['countPassengerRide'],
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.cancel_outlined,
|
||||||
|
'Canceled Rides',
|
||||||
|
data['countPassengerCancel'],
|
||||||
|
valueColor: Colors.redAccent,
|
||||||
|
),
|
||||||
|
_buildDetailTile(
|
||||||
|
Icons.rate_review_outlined,
|
||||||
|
'Feedback Given',
|
||||||
|
data['countFeedback'],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// --- Action Buttons ---
|
||||||
|
_buildActionButtons(
|
||||||
|
context, controller, data, isSuperAdmin),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Header with Gradient/White Background ---
|
||||||
|
Widget _buildHeaderSection(BuildContext context, Map<String, dynamic> data) {
|
||||||
|
String firstName = data['first_name'] ?? '';
|
||||||
|
String lastName = data['last_name'] ?? '';
|
||||||
|
String fullName = '$firstName $lastName'.trim();
|
||||||
|
if (fullName.isEmpty) fullName = "Passenger";
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 25),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(30)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 45,
|
||||||
|
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||||
|
child: Text(
|
||||||
|
fullName[0].toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 35,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
fullName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
data['status'] ?? 'Active',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.blue,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoCard(
|
||||||
|
{required String title,
|
||||||
|
required IconData icon,
|
||||||
|
required List<Widget> children}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.05),
|
||||||
|
spreadRadius: 2,
|
||||||
|
blurRadius: 10)
|
||||||
|
],
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'Email is ${data['email']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Icon(icon, color: AppColor.primaryColor, size: 22),
|
||||||
'Phone is ${data['phone']}',
|
const SizedBox(width: 10),
|
||||||
style: AppStyle.title,
|
Text(title.tr,
|
||||||
),
|
style: const TextStyle(
|
||||||
Text(
|
fontSize: 17, fontWeight: FontWeight.bold)),
|
||||||
'gender is ${data['gender']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Divider(height: 25, color: Colors.grey.withOpacity(0.2)),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
...children,
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'status is ${data['status']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'birthdate is ${data['birthdate']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
);
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailTile(IconData icon, String label, dynamic value,
|
||||||
|
{Color? valueColor}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'site is ${data['site']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'sosPhone is ${data['sosPhone']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Count Feedback is ${data['countFeedback']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Count Driver Rate is ${data['countDriverRate']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Count Cancel is ${data['countPassengerCancel']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Count Ride is ${data['countPassengerRide']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Rating Captain Avarage is ${data['passengerAverageRating']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'Rating is ${data['ratingPassenger']}',
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(width: 3, color: AppColor.yellowColor)),
|
color: Colors.grey[100],
|
||||||
child: TextButton(
|
borderRadius: BorderRadius.circular(8)),
|
||||||
onPressed: () async {
|
child: Icon(icon, color: Colors.grey[600], size: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label.tr,
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[500])),
|
||||||
|
Text(
|
||||||
|
value?.toString() ?? 'N/A',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: valueColor ?? Colors.black87),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButtons(
|
||||||
|
BuildContext context,
|
||||||
|
PassengerAdminController controller,
|
||||||
|
Map<String, dynamic> data,
|
||||||
|
bool isSuperAdmin) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// --- Send Notification (For All Admins) ---
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.notifications_active_outlined,
|
||||||
|
color: Colors.white),
|
||||||
|
label: Text("Send Notification".tr,
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColor.primaryColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
onPressed: () => _showSendNotificationDialog(controller, data),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- Edit/Delete (Super Admin Only) ---
|
||||||
|
if (isSuperAdmin) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.edit_note_rounded, size: 20),
|
||||||
|
label: Text("Edit".tr),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: AppColor.yellowColor,
|
||||||
|
elevation: 0,
|
||||||
|
side: BorderSide(color: AppColor.yellowColor),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// Get.to(() => const FormPassenger(), arguments: {
|
||||||
|
// 'isEditMode': true,
|
||||||
|
// 'passengerData': data,
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.delete_outline_rounded, size: 20),
|
||||||
|
label: Text("Delete".tr),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red[50],
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: () => _showDeleteConfirmation(data),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
// Message for normal admins
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
Text(
|
||||||
|
"Only Super Admins can edit or delete passengers.",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[400],
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: FontStyle.italic),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper: Format Phone (Last 4 digits for normal admin) ---
|
||||||
|
String _formatPhoneNumber(String phone, bool isSuperAdmin) {
|
||||||
|
if (isSuperAdmin) return phone;
|
||||||
|
if (phone.length <= 4) return phone;
|
||||||
|
return '${'*' * (phone.length - 4)}${phone.substring(phone.length - 4)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper: Mask Email ---
|
||||||
|
String _maskEmail(String? email) {
|
||||||
|
if (email == null || email.isEmpty) return 'N/A';
|
||||||
|
int atIndex = email.indexOf('@');
|
||||||
|
if (atIndex <= 1) return email; // Too short to mask
|
||||||
|
return '${email.substring(0, 2)}****${email.substring(atIndex)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSendNotificationDialog(
|
||||||
|
PassengerAdminController controller, Map<String, dynamic> data) {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'Send Notification'.tr,
|
title: 'Send Notification'.tr,
|
||||||
titleStyle: AppStyle.title,
|
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: key,
|
key: controller.formPrizeKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: titleNotify,
|
controller: controller.titleNotify,
|
||||||
label: 'title'.tr,
|
label: 'Title'.tr,
|
||||||
hint: 'title notificaton'.tr,
|
hint: 'Notification title'.tr,
|
||||||
type: TextInputType.name),
|
type: TextInputType.text),
|
||||||
const SizedBox(
|
const SizedBox(height: 10),
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
MyTextForm(
|
MyTextForm(
|
||||||
controller: bodyNotify,
|
controller: controller.bodyNotify,
|
||||||
label: 'body'.tr,
|
label: 'Body'.tr,
|
||||||
hint: 'body notificaton'.tr,
|
hint: 'Message body'.tr,
|
||||||
type: TextInputType.name)
|
type: TextInputType.text)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
confirm: MyElevatedButton(
|
confirm: SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: MyElevatedButton(
|
||||||
title: 'Send',
|
title: 'Send',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (key.currentState!.validate()) {
|
// Validate form safely
|
||||||
FirebaseMessagesController()
|
if (controller.formPrizeKey.currentState?.validate() ?? false) {
|
||||||
.sendNotificationToAnyWithoutData(
|
FirebaseMessagesController().sendNotificationToAnyWithoutData(
|
||||||
titleNotify.text,
|
controller.titleNotify.text,
|
||||||
bodyNotify.text,
|
controller.bodyNotify.text,
|
||||||
data['passengerToken'],
|
data['passengerToken'],
|
||||||
'order.wav');
|
'order.wav');
|
||||||
Get.back();
|
Get.back();
|
||||||
|
Get.snackbar('Success', 'Notification sent successfully!',
|
||||||
|
backgroundColor: Colors.green.withOpacity(0.2));
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
child: Text(
|
|
||||||
"Send Notificaion to Passenger ".tr,
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
cancel: TextButton(
|
||||||
],
|
onPressed: () => Get.back(),
|
||||||
|
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDeleteConfirmation(Map<String, dynamic> user) {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: 'Confirm Deletion'.tr,
|
||||||
|
titleStyle:
|
||||||
|
const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold),
|
||||||
|
middleText:
|
||||||
|
'Are you sure you want to delete ${user['first_name']}? This action cannot be undone.'
|
||||||
|
.tr,
|
||||||
|
confirm: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
||||||
|
onPressed: () async {
|
||||||
|
// 1. Close Dialog
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
// 2. Perform Delete Operation
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: AppLink.admin_delete_and_blacklist_passenger,
|
||||||
|
payload: {
|
||||||
|
'id': user['id'],
|
||||||
|
'phone': user['phone'],
|
||||||
|
'reason': 'Deleted by admin',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Handle Result
|
||||||
|
if (res['status'] == 'success') {
|
||||||
|
Get.back(); // Go back to list page
|
||||||
|
Get.snackbar('Deleted', 'Passenger removed successfully',
|
||||||
|
backgroundColor: Colors.red.withOpacity(0.2));
|
||||||
|
// Ideally, trigger a refresh on the controller here
|
||||||
|
// Get.find<PassengerAdminController>().getAll();
|
||||||
|
} else {
|
||||||
|
Get.snackbar('Error', res['message'] ?? 'Failed to delete',
|
||||||
|
backgroundColor: Colors.red.withOpacity(0.2));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('Delete'.tr, style: const TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
cancel: TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey)),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
|
||||||
isleading: true,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
697
lib/views/admin/rides/ride_lookup_page.dart
Normal file
697
lib/views/admin/rides/ride_lookup_page.dart
Normal file
@@ -0,0 +1,697 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart'; // ضروري للاتصال
|
||||||
|
import '../../../controller/functions/crud.dart';
|
||||||
|
import '../../../constant/box_name.dart'; // لتحديد هوية المستخدم الحالي
|
||||||
|
import '../../../main.dart'; // للوصول لـ box
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. MODEL
|
||||||
|
// ==========================================
|
||||||
|
class RideDashboardModel {
|
||||||
|
final String rideId;
|
||||||
|
final String status;
|
||||||
|
final String startLocation;
|
||||||
|
final String endLocation;
|
||||||
|
final String date;
|
||||||
|
final String time;
|
||||||
|
final String price;
|
||||||
|
final String distance;
|
||||||
|
|
||||||
|
final String driverId;
|
||||||
|
final String driverName;
|
||||||
|
final String driverPhone;
|
||||||
|
final String driverCompletedCount;
|
||||||
|
final String driverCanceledCount;
|
||||||
|
|
||||||
|
final String passengerName;
|
||||||
|
final String passengerPhone;
|
||||||
|
final String passengerCompletedCount;
|
||||||
|
|
||||||
|
final String cancelReason;
|
||||||
|
|
||||||
|
RideDashboardModel({
|
||||||
|
required this.rideId,
|
||||||
|
required this.status,
|
||||||
|
required this.startLocation,
|
||||||
|
required this.endLocation,
|
||||||
|
required this.date,
|
||||||
|
required this.time,
|
||||||
|
required this.price,
|
||||||
|
required this.distance,
|
||||||
|
required this.driverId,
|
||||||
|
required this.driverName,
|
||||||
|
required this.driverPhone,
|
||||||
|
required this.driverCompletedCount,
|
||||||
|
required this.driverCanceledCount,
|
||||||
|
required this.passengerName,
|
||||||
|
required this.passengerPhone,
|
||||||
|
required this.passengerCompletedCount,
|
||||||
|
required this.cancelReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RideDashboardModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RideDashboardModel(
|
||||||
|
rideId: json['id'].toString(),
|
||||||
|
status: json['status'] ?? '',
|
||||||
|
startLocation: json['start_location'] ?? '',
|
||||||
|
endLocation: json['end_location'] ?? '',
|
||||||
|
date: json['date'] ?? '',
|
||||||
|
time: json['time'] ?? '',
|
||||||
|
price: json['price']?.toString() ?? '0',
|
||||||
|
distance: json['distance']?.toString() ?? '0',
|
||||||
|
driverId: json['driver_id'].toString(),
|
||||||
|
driverName: json['driver_full_name'] ?? 'غير معروف',
|
||||||
|
driverPhone: json['d_phone'] ?? '',
|
||||||
|
driverCompletedCount: (json['d_completed'] ?? 0).toString(),
|
||||||
|
driverCanceledCount: (json['d_canceled'] ?? 0).toString(),
|
||||||
|
passengerName: json['passenger_full_name'] ?? 'غير معروف',
|
||||||
|
passengerPhone: json['p_phone'] ?? '',
|
||||||
|
passengerCompletedCount: (json['p_completed'] ?? 0).toString(),
|
||||||
|
cancelReason: json['cancel_reason'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LatLng? getStartLatLng() {
|
||||||
|
try {
|
||||||
|
var parts = startLocation.split(',');
|
||||||
|
return LatLng(double.parse(parts[0]), double.parse(parts[1]));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LatLng? getEndLatLng() {
|
||||||
|
try {
|
||||||
|
var parts = endLocation.split(',');
|
||||||
|
return LatLng(double.parse(parts[0]), double.parse(parts[1]));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. CONTROLLER
|
||||||
|
// ==========================================
|
||||||
|
class RidesListController extends GetxController {
|
||||||
|
var isLoading = false.obs;
|
||||||
|
var allRidesList = <RideDashboardModel>[];
|
||||||
|
var displayedRides = <RideDashboardModel>[].obs;
|
||||||
|
|
||||||
|
TextEditingController searchController = TextEditingController();
|
||||||
|
String currentStatus = 'Begin';
|
||||||
|
|
||||||
|
// === التحقق من صلاحية الأدمن ===
|
||||||
|
// نقرأ رقم الهاتف الحالي المخزن في التطبيق
|
||||||
|
String myPhone = box.read(BoxName.adminPhone)?.toString() ?? '';
|
||||||
|
|
||||||
|
bool get isSuperAdmin {
|
||||||
|
// ضع هنا أرقام هواتف الأدمن المسموح لهم برؤية الأرقام والاتصال
|
||||||
|
return myPhone == '963942542053' || myPhone == '963992952235';
|
||||||
|
}
|
||||||
|
|
||||||
|
final String apiUrl =
|
||||||
|
"https://api.intaleq.xyz/intaleq/Admin/rides/get_rides_by_status.php";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchRides();
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeTab(String status) {
|
||||||
|
currentStatus = status;
|
||||||
|
searchController.clear();
|
||||||
|
fetchRides();
|
||||||
|
}
|
||||||
|
|
||||||
|
void filterRides(String query) {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
displayedRides.value = allRidesList;
|
||||||
|
} else {
|
||||||
|
displayedRides.value = allRidesList.where((ride) {
|
||||||
|
return ride.driverPhone.contains(query) ||
|
||||||
|
ride.passengerPhone.contains(query) ||
|
||||||
|
ride.driverName.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
ride.passengerName.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
ride.rideId.contains(query);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchRides() async {
|
||||||
|
isLoading.value = true;
|
||||||
|
allRidesList.clear();
|
||||||
|
displayedRides.clear();
|
||||||
|
try {
|
||||||
|
var response =
|
||||||
|
await CRUD().post(link: apiUrl, payload: {"status": currentStatus});
|
||||||
|
if (response != 'failure' && response['status'] == 'success') {
|
||||||
|
List<dynamic> data = [];
|
||||||
|
if (response['message'] is List)
|
||||||
|
data = response['message'];
|
||||||
|
else if (response['data'] is List) data = response['data'];
|
||||||
|
|
||||||
|
allRidesList = data.map((e) => RideDashboardModel.fromJson(e)).toList();
|
||||||
|
displayedRides.value = allRidesList;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Error fetching rides: $e");
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. MAIN DASHBOARD SCREEN
|
||||||
|
// ==========================================
|
||||||
|
class RidesDashboardScreen extends StatelessWidget {
|
||||||
|
const RidesDashboardScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final controller = Get.put(RidesListController());
|
||||||
|
|
||||||
|
return DefaultTabController(
|
||||||
|
length: 4,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("مراقبة الرحلات"),
|
||||||
|
bottom: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
|
onTap: (index) {
|
||||||
|
List<String> statuses = ['Begin', 'New', 'Completed', 'Canceled'];
|
||||||
|
controller.changeTab(statuses[index]);
|
||||||
|
},
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: "جارية", icon: Icon(Icons.directions_car)),
|
||||||
|
Tab(text: "جديدة", icon: Icon(Icons.new_releases)),
|
||||||
|
Tab(text: "مكتملة", icon: Icon(Icons.check_circle)),
|
||||||
|
Tab(text: "ملغاة", icon: Icon(Icons.cancel)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.searchController,
|
||||||
|
onChanged: (val) => controller.filterRides(val),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "بحث...",
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() {
|
||||||
|
if (controller.isLoading.value)
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
if (controller.displayedRides.isEmpty)
|
||||||
|
return const Center(child: Text("لا توجد رحلات"));
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
itemCount: controller.displayedRides.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final ride = controller.displayedRides[index];
|
||||||
|
return _buildRideCard(
|
||||||
|
ride, controller.isSuperAdmin); // نمرر صلاحية الأدمن
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRideCard(RideDashboardModel ride, bool isAdmin) {
|
||||||
|
Color statusColor = _getStatusColor(ride.status);
|
||||||
|
String statusText = _getStatusText(ride.status);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
onTap: () => Get.to(() => RideMapMonitorScreen(
|
||||||
|
ride: ride, isAdmin: isAdmin)), // نمرر الصلاحية للخريطة أيضاً
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("رحلة #${ride.rideId}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: statusColor)),
|
||||||
|
child: Text(statusText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Stats
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade200)),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_statItem(
|
||||||
|
Icons.attach_money,
|
||||||
|
"السعر",
|
||||||
|
"${double.tryParse(ride.price)?.toStringAsFixed(0) ?? 0}",
|
||||||
|
Colors.green),
|
||||||
|
Container(
|
||||||
|
width: 1, height: 25, color: Colors.grey.shade300),
|
||||||
|
_statItem(
|
||||||
|
Icons.social_distance,
|
||||||
|
"المسافة",
|
||||||
|
"${double.tryParse(ride.distance)?.toStringAsFixed(1) ?? 0} كم",
|
||||||
|
Colors.blue),
|
||||||
|
Container(
|
||||||
|
width: 1, height: 25, color: Colors.grey.shade300),
|
||||||
|
_statItem(
|
||||||
|
Icons.access_time,
|
||||||
|
"الوقت",
|
||||||
|
ride.time.length > 5
|
||||||
|
? ride.time.substring(0, 5)
|
||||||
|
: ride.time,
|
||||||
|
Colors.orange),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_locationRow(Icons.my_location, ride.startLocation, Colors.blue),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_locationRow(Icons.location_on, ride.endLocation, Colors.red),
|
||||||
|
const Divider(height: 20),
|
||||||
|
|
||||||
|
// === معلومات السائق والراكب مع ميزة إخفاء الرقم ===
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _userInfo(
|
||||||
|
title: "الكابتن",
|
||||||
|
name: ride.driverName,
|
||||||
|
phone: ride.driverPhone,
|
||||||
|
isAdmin: isAdmin,
|
||||||
|
completed: ride.driverCompletedCount,
|
||||||
|
canceled: ride.driverCanceledCount)),
|
||||||
|
Container(width: 1, height: 40, color: Colors.grey.shade300),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: _userInfo(
|
||||||
|
title: "الراكب",
|
||||||
|
name: ride.passengerName,
|
||||||
|
phone: ride.passengerPhone,
|
||||||
|
isAdmin: isAdmin,
|
||||||
|
completed: ride.passengerCompletedCount)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if ((ride.status.contains('Cancel') ||
|
||||||
|
ride.status == 'TimeOut') &&
|
||||||
|
ride.cancelReason.isNotEmpty &&
|
||||||
|
ride.cancelReason != 'لا يوجد سبب') ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.red.shade200)),
|
||||||
|
child: Text("السبب: ${ride.cancelReason}",
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.red.shade900, fontSize: 13)),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ويدجت عرض المعلومات مع منطق الإخفاء ===
|
||||||
|
Widget _userInfo(
|
||||||
|
{required String title,
|
||||||
|
required String name,
|
||||||
|
required String phone,
|
||||||
|
required bool isAdmin,
|
||||||
|
String? completed,
|
||||||
|
String? canceled}) {
|
||||||
|
// 1. منطق الإخفاء (Masking)
|
||||||
|
String displayPhone = phone;
|
||||||
|
if (!isAdmin && phone.length > 4) {
|
||||||
|
// إظهار آخر 4 أرقام فقط
|
||||||
|
displayPhone =
|
||||||
|
phone.substring(phone.length - 4).padLeft(phone.length, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
|
Text(name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
|
||||||
|
// 2. رقم الهاتف وزر الاتصال
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(displayPhone,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11, color: Colors.grey, letterSpacing: 1)),
|
||||||
|
if (isAdmin && phone.isNotEmpty) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => _makePhoneCall(phone),
|
||||||
|
child: const Icon(Icons.call, size: 14, color: Colors.green),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (completed != null)
|
||||||
|
Text("تم: $completed ${canceled != null ? '| ألغى: $canceled' : ''}",
|
||||||
|
style: const TextStyle(fontSize: 9, color: Colors.black54)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _makePhoneCall(String phoneNumber) async {
|
||||||
|
// التحقق مما إذا كانت العلامة موجودة مسبقاً لتجنب التكرار (++963)
|
||||||
|
String formattedPhone = phoneNumber;
|
||||||
|
if (!formattedPhone.startsWith('+')) {
|
||||||
|
formattedPhone = '+$formattedPhone';
|
||||||
|
}
|
||||||
|
|
||||||
|
final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone);
|
||||||
|
|
||||||
|
if (await canLaunchUrl(launchUri)) {
|
||||||
|
await launchUrl(launchUri);
|
||||||
|
} else {
|
||||||
|
// يمكنك هنا إضافة تنبيه بسيط في حال فشل فتح التطبيق
|
||||||
|
debugPrint("لا يمكن الاتصال بالرقم: $formattedPhone");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
Color _getStatusColor(String s) {
|
||||||
|
if (s == 'Begin' || s == 'Arrived') return Colors.green;
|
||||||
|
if (s == 'Finished') return Colors.teal;
|
||||||
|
if (s.contains('Cancel') || s == 'TimeOut') return Colors.red;
|
||||||
|
if (s == 'New') return Colors.blue;
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getStatusText(String s) {
|
||||||
|
if (s == 'Begin' || s == 'Arrived') return "جارية 🟢";
|
||||||
|
if (s == 'Finished') return "مكتملة ✅";
|
||||||
|
if (s == 'CancelFromDriver' || s == 'CancelFromDriverAfterApply')
|
||||||
|
return "ألغاها السائق 👨✈️";
|
||||||
|
if (s == 'CancelFromPassenger') return "ألغاها الراكب 👤";
|
||||||
|
if (s == 'TimeOut') return "انتهى الوقت ⏱️";
|
||||||
|
if (s == 'New') return "جديدة 🆕";
|
||||||
|
return "ملغاة ❌";
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _statItem(IconData icon, String label, String value, Color color) {
|
||||||
|
return Column(children: [
|
||||||
|
Icon(icon, size: 18, color: color),
|
||||||
|
Text(value,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _locationRow(IconData icon, String text, Color color) {
|
||||||
|
return Row(children: [
|
||||||
|
Icon(icon, size: 16, color: color),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 13)))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. MAP MONITOR SCREEN
|
||||||
|
// ==========================================
|
||||||
|
class RideMapMonitorScreen extends StatefulWidget {
|
||||||
|
final RideDashboardModel ride;
|
||||||
|
final bool isAdmin; // نستقبل الصلاحية هنا أيضاً
|
||||||
|
const RideMapMonitorScreen(
|
||||||
|
{super.key, required this.ride, required this.isAdmin});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RideMapMonitorScreen> createState() => _RideMapMonitorScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RideMapMonitorScreenState extends State<RideMapMonitorScreen> {
|
||||||
|
final MapController mapController = MapController();
|
||||||
|
LatLng? startPos, endPos, driverPos;
|
||||||
|
Timer? _timer;
|
||||||
|
bool isFirstLoad = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
startPos = widget.ride.getStartLatLng();
|
||||||
|
endPos = widget.ride.getEndLatLng();
|
||||||
|
if (widget.ride.status == 'Begin' || widget.ride.status == 'Arrived') {
|
||||||
|
fetchDriverLocation();
|
||||||
|
_timer = Timer.periodic(
|
||||||
|
const Duration(seconds: 10), (_) => fetchDriverLocation());
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _fitBounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
mapController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _fitBounds() {
|
||||||
|
List<LatLng> points = [];
|
||||||
|
if (startPos != null) points.add(startPos!);
|
||||||
|
if (endPos != null) points.add(endPos!);
|
||||||
|
if (driverPos != null) points.add(driverPos!);
|
||||||
|
if (points.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
mapController.fitCamera(CameraFit.bounds(
|
||||||
|
bounds: LatLngBounds.fromPoints(points),
|
||||||
|
padding: const EdgeInsets.all(50)));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchDriverLocation() async {
|
||||||
|
String trackUrl =
|
||||||
|
"https://api.intaleq.xyz/intaleq/Admin/rides/get_driver_live_pos.php";
|
||||||
|
try {
|
||||||
|
var response = await CRUD()
|
||||||
|
.post(link: trackUrl, payload: {"driver_id": widget.ride.driverId});
|
||||||
|
if (response != 'failure') {
|
||||||
|
var d = response['message'];
|
||||||
|
setState(() {
|
||||||
|
driverPos = LatLng(double.parse(d['latitude'].toString()),
|
||||||
|
double.parse(d['longitude'].toString()));
|
||||||
|
});
|
||||||
|
if (isFirstLoad) {
|
||||||
|
_fitBounds();
|
||||||
|
isFirstLoad = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("تتبع الرحلة #${widget.ride.rideId}"),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
elevation: 1),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
FlutterMap(
|
||||||
|
mapController: mapController,
|
||||||
|
options: MapOptions(
|
||||||
|
initialCenter: startPos ?? const LatLng(33.513, 36.276),
|
||||||
|
initialZoom: 13),
|
||||||
|
children: [
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate:
|
||||||
|
'https://tile.openstreetmap.org/{z}/{x}/{y}.png'),
|
||||||
|
if (startPos != null && endPos != null)
|
||||||
|
PolylineLayer(polylines: [
|
||||||
|
Polyline(
|
||||||
|
points: [startPos!, endPos!],
|
||||||
|
strokeWidth: 4,
|
||||||
|
color: Colors.blue.withOpacity(0.7))
|
||||||
|
]),
|
||||||
|
MarkerLayer(markers: [
|
||||||
|
if (startPos != null)
|
||||||
|
Marker(
|
||||||
|
point: startPos!,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child:
|
||||||
|
const Icon(Icons.flag, color: Colors.green, size: 40),
|
||||||
|
alignment: Alignment.topCenter),
|
||||||
|
if (endPos != null)
|
||||||
|
Marker(
|
||||||
|
point: endPos!,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: const Icon(Icons.location_on,
|
||||||
|
color: Colors.red, size: 40),
|
||||||
|
alignment: Alignment.topCenter),
|
||||||
|
if (driverPos != null)
|
||||||
|
Marker(
|
||||||
|
point: driverPos!,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(blurRadius: 5, color: Colors.black26)
|
||||||
|
]),
|
||||||
|
child: const Icon(Icons.directions_car,
|
||||||
|
color: Colors.blue, size: 30))),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 20,
|
||||||
|
left: 15,
|
||||||
|
right: 15,
|
||||||
|
child: Card(
|
||||||
|
elevation: 5,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(15.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text("السعر: ${widget.ride.price}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.green)),
|
||||||
|
Text("المسافة: ${widget.ride.distance} كم")
|
||||||
|
]),
|
||||||
|
const Divider(),
|
||||||
|
_mapInfo(Icons.person, "الكابتن: ${widget.ride.driverName}",
|
||||||
|
widget.ride.driverPhone),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_mapInfo(
|
||||||
|
Icons.person_outline,
|
||||||
|
"الراكب: ${widget.ride.passengerName}",
|
||||||
|
widget.ride.passengerPhone),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
_simpleInfo(
|
||||||
|
Icons.my_location, "من: ${widget.ride.startLocation}"),
|
||||||
|
_simpleInfo(
|
||||||
|
Icons.location_on, "إلى: ${widget.ride.endLocation}"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
mini: true,
|
||||||
|
child: const Icon(Icons.center_focus_strong),
|
||||||
|
onPressed: _fitBounds),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ويدجت خاصة بالخريطة تطبق نفس منطق الإخفاء
|
||||||
|
Widget _mapInfo(IconData icon, String text, String phone) {
|
||||||
|
String displayPhone = phone;
|
||||||
|
if (!widget.isAdmin && phone.length > 4) {
|
||||||
|
displayPhone =
|
||||||
|
phone.substring(phone.length - 4).padLeft(phone.length, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: Colors.grey[700]),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text("$text ($displayPhone)",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 14))),
|
||||||
|
if (widget.isAdmin && phone.isNotEmpty)
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
final Uri launchUri = Uri(scheme: 'tel', path: phone);
|
||||||
|
if (await canLaunchUrl(launchUri)) await launchUrl(launchUri);
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.call, size: 18, color: Colors.green),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _simpleInfo(IconData icon, String text) {
|
||||||
|
return Row(children: [
|
||||||
|
Icon(icon, size: 18, color: Colors.grey[700]),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(text,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 14)))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
lib/views/admin/static/notes_driver_page.dart
Normal file
163
lib/views/admin/static/notes_driver_page.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart' hide TextDirection;
|
||||||
|
import 'package:sefer_admin1/controller/functions/launch.dart';
|
||||||
|
|
||||||
|
import '../../../controller/admin/static_controller.dart';
|
||||||
|
import '../../widgets/mycircular.dart';
|
||||||
|
|
||||||
|
class DailyNotesView extends StatelessWidget {
|
||||||
|
const DailyNotesView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// نستخدم نفس الكونترولر للوصول لدالة جلب الملاحظات
|
||||||
|
final controller = Get.find<StaticController>();
|
||||||
|
|
||||||
|
// عند فتح الصفحة، نجلب ملاحظات اليوم الحالي
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
controller.fetchDailyNotes(DateTime.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF0F2F5),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'سجل المكالمات اليومي',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF1A1A1A),
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
iconTheme: const IconThemeData(color: Colors.black87),
|
||||||
|
),
|
||||||
|
body: GetBuilder<StaticController>(
|
||||||
|
builder: (controller) {
|
||||||
|
if (controller.isLoadingNotes) {
|
||||||
|
return const Center(child: MyCircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.dailyNotesList.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.note_alt_outlined,
|
||||||
|
size: 80, color: Colors.grey.shade300),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text("لا توجد سجلات لهذا اليوم",
|
||||||
|
style: TextStyle(color: Colors.grey.shade600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: controller.dailyNotesList.length,
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final note = controller.dailyNotesList[index];
|
||||||
|
final String name = note['editor'] ?? note['name'] ?? 'Unknown';
|
||||||
|
final String phone = note['phone'] ?? note['phone'] ?? 'Unknown';
|
||||||
|
final String content = note['note'] ?? note['content'] ?? '';
|
||||||
|
final String time = note['createdAt'] ?? '';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
border: Border(
|
||||||
|
right: BorderSide(
|
||||||
|
color: _getEmployeeColor(name), width: 4))),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor:
|
||||||
|
_getEmployeeColor(name).withOpacity(0.1),
|
||||||
|
child: Icon(Icons.person,
|
||||||
|
size: 16, color: _getEmployeeColor(name)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
name.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 100),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
makePhoneCall('+$phone');
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
phone,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
fontSize: 14),
|
||||||
|
),
|
||||||
|
Icon(Icons.phone)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 22),
|
||||||
|
Text(
|
||||||
|
time.split(' ').last, // عرض الوقت فقط
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade400, fontSize: 12),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 20),
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
height: 1.5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getEmployeeColor(String name) {
|
||||||
|
String n = name.toLowerCase().trim();
|
||||||
|
if (n.contains('shahd')) return Colors.redAccent;
|
||||||
|
if (n.contains('mayar')) return Colors.amber.shade700;
|
||||||
|
if (n.contains('rama2')) return Colors.green;
|
||||||
|
if (n.contains('rama1')) return Colors.blue;
|
||||||
|
return Colors.blueGrey;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ import 'package:sefer_admin1/env/env.dart';
|
|||||||
|
|
||||||
import '../../controller/auth/login_controller.dart';
|
import '../../controller/auth/login_controller.dart';
|
||||||
import '../../controller/auth/otp_helper.dart';
|
import '../../controller/auth/otp_helper.dart';
|
||||||
|
import '../../controller/functions/crud.dart';
|
||||||
|
import '../../print.dart';
|
||||||
|
|
||||||
class AdminLoginPage extends StatefulWidget {
|
class AdminLoginPage extends StatefulWidget {
|
||||||
const AdminLoginPage({super.key});
|
const AdminLoginPage({super.key});
|
||||||
@@ -17,6 +19,7 @@ class _AdminLoginPageState extends State<AdminLoginPage> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
final allowedPhones = Env.ALLOWED_ADMIN_PHONES;
|
final allowedPhones = Env.ALLOWED_ADMIN_PHONES;
|
||||||
|
Log.print('allowedPhones: ${allowedPhones}');
|
||||||
allowedPhones.toString().split(',');
|
allowedPhones.toString().split(',');
|
||||||
|
|
||||||
final phone = _phoneController.text.trim();
|
final phone = _phoneController.text.trim();
|
||||||
@@ -36,6 +39,16 @@ class _AdminLoginPageState extends State<AdminLoginPage> {
|
|||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initializeToken(); // استدعاء دالة async بدون await
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeToken() async {
|
||||||
|
await CRUD().getJWT();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(OtpHelper());
|
Get.put(OtpHelper());
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
|||||||
final driverID = '123'; // ← عدّله حسب نظامك
|
final driverID = '123'; // ← عدّله حسب نظامك
|
||||||
final invoiceNumber = generateInvoiceNumber();
|
final invoiceNumber = generateInvoiceNumber();
|
||||||
final amount = _amountController.text.trim();
|
final amount = _amountController.text.trim();
|
||||||
|
final itemName = _itemNameController.text.trim();
|
||||||
final date = DateTime.now().toIso8601String().split('T').first;
|
final date = DateTime.now().toIso8601String().split('T').first;
|
||||||
|
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
@@ -54,6 +55,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
|||||||
..fields['driverID'] = driverID
|
..fields['driverID'] = driverID
|
||||||
..fields['invoiceNumber'] = invoiceNumber
|
..fields['invoiceNumber'] = invoiceNumber
|
||||||
..fields['amount'] = amount
|
..fields['amount'] = amount
|
||||||
|
..fields['name'] = itemName
|
||||||
..fields['date'] = date
|
..fields['date'] = date
|
||||||
..headers.addAll(headers);
|
..headers.addAll(headers);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class MyTextForm extends StatelessWidget {
|
|||||||
return 'Please enter a valid email.'.tr;
|
return 'Please enter a valid email.'.tr;
|
||||||
}
|
}
|
||||||
} else if (type == TextInputType.phone) {
|
} else if (type == TextInputType.phone) {
|
||||||
if (value.length != 11) {
|
if (value.length != 10) {
|
||||||
return 'Please enter a valid phone number.'.tr;
|
return 'Please enter a valid phone number.'.tr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
platform :osx, '10.14'
|
platform :osx, '10.15'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
250
macos/Podfile.lock
Normal file
250
macos/Podfile.lock
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
PODS:
|
||||||
|
- AppAuth (1.7.6):
|
||||||
|
- AppAuth/Core (= 1.7.6)
|
||||||
|
- AppAuth/ExternalUserAgent (= 1.7.6)
|
||||||
|
- AppAuth/Core (1.7.6)
|
||||||
|
- AppAuth/ExternalUserAgent (1.7.6):
|
||||||
|
- AppAuth/Core
|
||||||
|
- AppCheckCore (11.2.0):
|
||||||
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
|
- device_info_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- file_selector_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- Firebase/CoreOnly (11.15.0):
|
||||||
|
- FirebaseCore (~> 11.15.0)
|
||||||
|
- Firebase/Crashlytics (11.15.0):
|
||||||
|
- Firebase/CoreOnly
|
||||||
|
- FirebaseCrashlytics (~> 11.15.0)
|
||||||
|
- Firebase/Messaging (11.15.0):
|
||||||
|
- Firebase/CoreOnly
|
||||||
|
- FirebaseMessaging (~> 11.15.0)
|
||||||
|
- firebase_core (3.15.2):
|
||||||
|
- Firebase/CoreOnly (~> 11.15.0)
|
||||||
|
- FlutterMacOS
|
||||||
|
- firebase_crashlytics (4.3.10):
|
||||||
|
- Firebase/CoreOnly (~> 11.15.0)
|
||||||
|
- Firebase/Crashlytics (~> 11.15.0)
|
||||||
|
- firebase_core
|
||||||
|
- FlutterMacOS
|
||||||
|
- firebase_messaging (15.2.10):
|
||||||
|
- Firebase/CoreOnly (~> 11.15.0)
|
||||||
|
- Firebase/Messaging (~> 11.15.0)
|
||||||
|
- firebase_core
|
||||||
|
- FlutterMacOS
|
||||||
|
- FirebaseCore (11.15.0):
|
||||||
|
- FirebaseCoreInternal (~> 11.15.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
|
- FirebaseCoreExtension (11.15.0):
|
||||||
|
- FirebaseCore (~> 11.15.0)
|
||||||
|
- FirebaseCoreInternal (11.15.0):
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
|
- FirebaseCrashlytics (11.15.0):
|
||||||
|
- FirebaseCore (~> 11.15.0)
|
||||||
|
- FirebaseInstallations (~> 11.0)
|
||||||
|
- FirebaseRemoteConfigInterop (~> 11.0)
|
||||||
|
- FirebaseSessions (~> 11.0)
|
||||||
|
- GoogleDataTransport (~> 10.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
|
- FirebaseInstallations (11.15.0):
|
||||||
|
- FirebaseCore (~> 11.15.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
|
- FirebaseMessaging (11.15.0):
|
||||||
|
- FirebaseCore (~> 11.15.0)
|
||||||
|
- FirebaseInstallations (~> 11.0)
|
||||||
|
- GoogleDataTransport (~> 10.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/Reachability (~> 8.1)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- FirebaseRemoteConfigInterop (11.15.0)
|
||||||
|
- FirebaseSessions (11.15.0):
|
||||||
|
- FirebaseCore (~> 11.15.0)
|
||||||
|
- FirebaseCoreExtension (~> 11.15.0)
|
||||||
|
- FirebaseInstallations (~> 11.0)
|
||||||
|
- GoogleDataTransport (~> 10.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- PromisesSwift (~> 2.1)
|
||||||
|
- flutter_image_compress_macos (1.0.0):
|
||||||
|
- FlutterMacOS
|
||||||
|
- flutter_secure_storage_macos (6.1.3):
|
||||||
|
- FlutterMacOS
|
||||||
|
- FlutterMacOS (1.0.0)
|
||||||
|
- google_sign_in_ios (0.0.1):
|
||||||
|
- AppAuth (>= 1.7.4)
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- GoogleSignIn (~> 8.0)
|
||||||
|
- GTMSessionFetcher (>= 3.4.0)
|
||||||
|
- GoogleDataTransport (10.1.0):
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
|
- GoogleSignIn (8.0.0):
|
||||||
|
- AppAuth (< 2.0, >= 1.7.3)
|
||||||
|
- AppCheckCore (~> 11.0)
|
||||||
|
- GTMAppAuth (< 5.0, >= 4.1.1)
|
||||||
|
- GTMSessionFetcher/Core (~> 3.3)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Network
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Logger (8.1.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Network (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- "GoogleUtilities/NSData+zlib"
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Reachability
|
||||||
|
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Privacy (8.1.0)
|
||||||
|
- GoogleUtilities/Reachability (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GTMAppAuth (4.1.1):
|
||||||
|
- AppAuth/Core (~> 1.7)
|
||||||
|
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||||
|
- GTMSessionFetcher (3.5.0):
|
||||||
|
- GTMSessionFetcher/Full (= 3.5.0)
|
||||||
|
- GTMSessionFetcher/Core (3.5.0)
|
||||||
|
- GTMSessionFetcher/Full (3.5.0):
|
||||||
|
- GTMSessionFetcher/Core
|
||||||
|
- local_auth_darwin (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- nanopb (3.30910.0):
|
||||||
|
- nanopb/decode (= 3.30910.0)
|
||||||
|
- nanopb/encode (= 3.30910.0)
|
||||||
|
- nanopb/decode (3.30910.0)
|
||||||
|
- nanopb/encode (3.30910.0)
|
||||||
|
- path_provider_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- PromisesObjC (2.4.0)
|
||||||
|
- PromisesSwift (2.4.0):
|
||||||
|
- PromisesObjC (= 2.4.0)
|
||||||
|
- sqflite_darwin (0.0.4):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- url_launcher_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||||
|
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||||
|
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||||
|
- firebase_crashlytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos`)
|
||||||
|
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||||
|
- flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`)
|
||||||
|
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||||
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- google_sign_in_ios (from `Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin`)
|
||||||
|
- local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- AppAuth
|
||||||
|
- AppCheckCore
|
||||||
|
- Firebase
|
||||||
|
- FirebaseCore
|
||||||
|
- FirebaseCoreExtension
|
||||||
|
- FirebaseCoreInternal
|
||||||
|
- FirebaseCrashlytics
|
||||||
|
- FirebaseInstallations
|
||||||
|
- FirebaseMessaging
|
||||||
|
- FirebaseRemoteConfigInterop
|
||||||
|
- FirebaseSessions
|
||||||
|
- GoogleDataTransport
|
||||||
|
- GoogleSignIn
|
||||||
|
- GoogleUtilities
|
||||||
|
- GTMAppAuth
|
||||||
|
- GTMSessionFetcher
|
||||||
|
- nanopb
|
||||||
|
- PromisesObjC
|
||||||
|
- PromisesSwift
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
device_info_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||||
|
file_selector_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||||
|
firebase_core:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
|
||||||
|
firebase_crashlytics:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_crashlytics/macos
|
||||||
|
firebase_messaging:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
||||||
|
flutter_image_compress_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos
|
||||||
|
flutter_secure_storage_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||||
|
FlutterMacOS:
|
||||||
|
:path: Flutter/ephemeral
|
||||||
|
google_sign_in_ios:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin
|
||||||
|
local_auth_darwin:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin
|
||||||
|
path_provider_foundation:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||||
|
sqflite_darwin:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||||
|
url_launcher_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
||||||
|
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||||
|
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||||
|
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||||
|
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
|
||||||
|
firebase_core: 7667f880631ae8ad10e3d6567ab7582fe0682326
|
||||||
|
firebase_crashlytics: af8dce4a4f3b2b1556bf51043623060a5fc7eca7
|
||||||
|
firebase_messaging: df39858bcbbcce792c9e4f1ca51b41123d6181fd
|
||||||
|
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
|
||||||
|
FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435
|
||||||
|
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
|
||||||
|
FirebaseCrashlytics: e09d0bc19aa54a51e45b8039c836ef73f32c039a
|
||||||
|
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
|
||||||
|
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
|
||||||
|
FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa
|
||||||
|
FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7
|
||||||
|
flutter_image_compress_macos: e68daf54bb4bf2144c580fd4d151c949cbf492f0
|
||||||
|
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
||||||
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
|
google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
|
||||||
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
|
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
|
||||||
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
|
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||||
|
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||||
|
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
||||||
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
|
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
@@ -21,12 +21,14 @@
|
|||||||
/* End PBXAggregateTarget section */
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
17D3E71566D6F280AC1497ED /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 529702085A98989612EF4B94 /* Pods_Runner.framework */; };
|
||||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
|
D35F436D72E4B2EDD1E1BBEC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A0BEBB6BD55D52332CA81ED /* Pods_RunnerTests.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -60,11 +62,12 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
2BE9293BC2E10C9D5F0F08B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||||
33CC10ED2044A3C60003C045 /* intaleq_admin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "intaleq_admin.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
33CC10ED2044A3C60003C045 /* intaleq_admin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = intaleq_admin.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
@@ -76,8 +79,15 @@
|
|||||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
|
4A0BEBB6BD55D52332CA81ED /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
529702085A98989612EF4B94 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
57D62622DF9F1AFCA371D456 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
693E736CE390D9EAD52093D9 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
991BB790141B213D8BF9E75C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
996CBF94A18BBCF3EEC882E7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
AB6DA36D2BAECCF1DC13A82F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -85,6 +95,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D35F436D72E4B2EDD1E1BBEC /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -92,6 +103,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
17D3E71566D6F280AC1497ED /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -124,7 +136,8 @@
|
|||||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||||
33CC10EE2044A3C60003C045 /* Products */,
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
442C92138252D77B90B180F9 /* Pods */,
|
||||||
|
41EA67A3A7FCB1DDADFB314F /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -172,13 +185,29 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
41EA67A3A7FCB1DDADFB314F /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
529702085A98989612EF4B94 /* Pods_Runner.framework */,
|
||||||
|
4A0BEBB6BD55D52332CA81ED /* Pods_RunnerTests.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
442C92138252D77B90B180F9 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
AB6DA36D2BAECCF1DC13A82F /* Pods-Runner.debug.xcconfig */,
|
||||||
|
2BE9293BC2E10C9D5F0F08B9 /* Pods-Runner.release.xcconfig */,
|
||||||
|
991BB790141B213D8BF9E75C /* Pods-Runner.profile.xcconfig */,
|
||||||
|
996CBF94A18BBCF3EEC882E7 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
57D62622DF9F1AFCA371D456 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
693E736CE390D9EAD52093D9 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -186,6 +215,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
FF11494ED183929E30BE29BC /* [CP] Check Pods Manifest.lock */,
|
||||||
331C80D1294CF70F00263BE5 /* Sources */,
|
331C80D1294CF70F00263BE5 /* Sources */,
|
||||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||||
331C80D3294CF70F00263BE5 /* Resources */,
|
331C80D3294CF70F00263BE5 /* Resources */,
|
||||||
@@ -204,11 +234,14 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
5B019689435FA2B04EF137C6 /* [CP] Check Pods Manifest.lock */,
|
||||||
33CC10E92044A3C60003C045 /* Sources */,
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
33CC10EB2044A3C60003C045 /* Resources */,
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
|
3FC083AA4C1F1997BBCE63BC /* [CP] Embed Pods Frameworks */,
|
||||||
|
6131D245E36334FE0924BDA0 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -329,6 +362,84 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||||
};
|
};
|
||||||
|
3FC083AA4C1F1997BBCE63BC /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
5B019689435FA2B04EF137C6 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
6131D245E36334FE0924BDA0 /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
FF11494ED183929E30BE29BC /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -380,6 +491,7 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 996CBF94A18BBCF3EEC882E7 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -394,6 +506,7 @@
|
|||||||
};
|
};
|
||||||
331C80DC294CF71000263BE5 /* Release */ = {
|
331C80DC294CF71000263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 57D62622DF9F1AFCA371D456 /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -408,6 +521,7 @@
|
|||||||
};
|
};
|
||||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 693E736CE390D9EAD52093D9 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -461,7 +575,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@@ -543,7 +657,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -593,7 +707,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
|||||||
@@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true />
|
<true />
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true />
|
<true />
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true />
|
<true />
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true />
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
|||||||
@@ -4,5 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true />
|
<true />
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
80
pubspec.lock
80
pubspec.lock
@@ -201,6 +201,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.6"
|
||||||
|
dart_earcut:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_earcut
|
||||||
|
sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -478,6 +486,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_map:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_map
|
||||||
|
sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -824,6 +840,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
latlong2:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: latlong2
|
||||||
|
sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.1"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -856,6 +880,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
|
lists:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lists
|
||||||
|
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
local_auth:
|
local_auth:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -896,6 +928,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.11"
|
version: "1.0.11"
|
||||||
|
logger:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logger
|
||||||
|
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.2"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -928,6 +968,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
mgrs_dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mgrs_dart
|
||||||
|
sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1032,6 +1080,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.1"
|
version: "3.9.1"
|
||||||
|
polylabel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: polylabel
|
||||||
|
sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1048,6 +1104,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.0.3"
|
||||||
|
proj4dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: proj4dart
|
||||||
|
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1228,6 +1292,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
unicode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: unicode
|
||||||
|
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1356,6 +1428,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
wkt_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: wkt_parser
|
||||||
|
sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ dependencies:
|
|||||||
get_storage: ^2.1.1
|
get_storage: ^2.1.1
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
# google_maps_flutter: ^2.6.1
|
# google_maps_flutter: ^2.6.1
|
||||||
|
flutter_map: ^7.0.0 # مكتبة OpenStreetMap للفلاتر
|
||||||
|
latlong2: ^0.9.1
|
||||||
google_sign_in: ^6.2.1
|
google_sign_in: ^6.2.1
|
||||||
http: ^1.0.0
|
http: ^1.0.0
|
||||||
# image: ^4.1.7
|
# image: ^4.1.7
|
||||||
|
|||||||
Reference in New Issue
Block a user