Initial commit for intaleq_admin

This commit is contained in:
Hamza-Ayed
2026-01-20 23:39:59 +03:00
parent 0b17f93aaa
commit a367bc7e5c
53 changed files with 20383 additions and 14662 deletions

2
.env
View File

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

View File

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

View File

@@ -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>
<!-- 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> </intent>
</queries> </queries>
</manifest>
</manifest>

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ---
isLoading = true; bool isLoadingNotes = false;
update(); // Notify the observers about the loading state change List<dynamic> dailyNotesList = [];
var res = await CRUD().get( // --- Chart Data (Current Range) ---
link: AppLink.getPassengersStatic, List<FlSpot> chartDataPassengers = [];
payload: {}, List<FlSpot> chartDataDrivers = [];
); List<FlSpot> chartDataRides = [];
jsonData1 = jsonDecode(res); List<FlSpot> chartDataDriversMatchingNotes = [];
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
isLoading = false;
final List<dynamic> jsonData = jsonResponse['message'];
totalMonthlyPassengers = jsonData[0]['totalMonthly'].toString();
passengersData = jsonData.map<MonthlyPassengerInstall>((item) {
return MonthlyPassengerInstall.fromJson(item);
}).toList();
final List<FlSpot> spots = passengersData
.map((data) => FlSpot(
data.day.toDouble(),
data.totalPassengers.toDouble(),
))
.toList();
chartDataPassengers = spots;
update(); // Notify the observers about the data and loading state change // 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();
} }
Future<void> fetchRides() async { // --- Helpers for View ---
isLoading = true; double get daysInPeriod {
update(); // Notify the observers about the loading state change if (startDate == null || endDate == null) return 31;
return endDate!.difference(startDate!).inDays + 1.0;
var res = await CRUD().get(
link: AppLink.getRidesStatic,
payload: {},
);
jsonData1 = jsonDecode(res);
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
isLoading = false;
final List<dynamic> jsonData = jsonResponse['message'];
totalMonthlyRides = jsonData[0]['totalMonthly'].toString();
ridesData = jsonData.map<MonthlyRidesInstall>((item) {
return MonthlyRidesInstall.fromJson(item);
}).toList();
final List<FlSpot> spots = ridesData
.map((data) => FlSpot(
data.day.toDouble(),
data.totalRides.toDouble(),
))
.toList();
chartDataRides = spots;
update(); // Notify the observers about the data and loading state change
} }
Future<void> fetchEmployee() async { String get currentDateString {
try { if (startDate == null || endDate == null) return "";
isLoading = true; return "${DateFormat('yyyy-MM-dd').format(startDate!)} : ${DateFormat('yyyy-MM-dd').format(endDate!)}";
update(); }
var res = await CRUD().get(link: AppLink.getEmployeeStatic, payload: {}); // --- Date Actions ---
void updateDateRange(DateTime start, DateTime end) {
startDate = start;
endDate = end;
if (isComparing) _calculateCompareDates();
getAll();
update();
}
// First check if the response is valid JSON void _calculateCompareDates() {
if (res == 'failure') { if (startDate == null || endDate == null) return;
throw FormatException('Invalid response: $res'); Duration duration = endDate!.difference(startDate!);
} compareEndDate = startDate!.subtract(const Duration(days: 1));
compareStartDate = compareEndDate!.subtract(duration);
}
var jsonResponse = jsonDecode(res) as Map<String, dynamic>; Future<void> toggleComparison() async {
isComparing = !isComparing;
if (isComparing) {
_calculateCompareDates();
} else {
compareStartDate = null;
compareEndDate = null;
_clearComparisonData();
}
await getAll();
}
// Initialize empty lists for all chart data void _clearComparisonData() {
chartDataEmployeeMaryam = <FlSpot>[]; chartDataPassengersCompare.clear();
chartDataEmployeeRawda = <FlSpot>[]; chartDataDriversCompare.clear();
chartDataEmployeeMena = <FlSpot>[]; chartDataRidesCompare.clear();
chartDataEmployeeSefer4 = <FlSpot>[]; chartDataDriversMatchingNotesCompare.clear();
totalMonthlyRides = '0';
// Check for error response chartDataEmployeerama1Compare.clear();
if (jsonResponse['status'] == 'failure') { chartDataEmployeeshahdCompare.clear();
isLoading = false; chartDataEmployeeRama2Compare.clear();
update(); chartDataEmployeeSefer4Compare.clear();
return;
}
final List<dynamic> jsonData = jsonResponse['message']; chartDataCallsrama1Compare.clear();
if (jsonData.isEmpty) { chartDataCallsShahdCompare.clear();
isLoading = false; chartDataCallsRama2Compare.clear();
update(); chartDataCallsSefer4Compare.clear();
return; }
}
totalMonthlyRides = jsonData[0]['totalMonthly']?.toString() ?? '0'; 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(),
};
}
// Group data by employee // --- Main Fetch Logic ---
Map<String, List<MonthlyEmployeeData>> employeeDataMap = {}; Future getAll() async {
if (startDate == null || endDate == null) return;
for (var item in jsonData) { isLoading = true;
var employeeData = MonthlyEmployeeData.fromJson(item); update();
if (!employeeDataMap.containsKey(employeeData.name)) {
employeeDataMap[employeeData.name] = [];
}
employeeDataMap[employeeData.name]!.add(employeeData);
}
final today = DateTime.now().day; await Future.wait([
fetchPassengers(isCompare: false),
fetchRides(isCompare: false),
fetchDrivers(isCompare: false),
fetchEmployee(isCompare: false),
fetchEditorCalls(isCompare: false),
fetchEmploymentStats(),
]);
// Create data for each employee if (isComparing && compareStartDate != null && compareEndDate != null) {
final employeeNames = { await Future.wait([
'maryam': chartDataEmployeeMaryam, fetchPassengers(isCompare: true),
'yasmine': chartDataEmployeeRawda, fetchRides(isCompare: true),
'mena': chartDataEmployeeMena, fetchDrivers(isCompare: true),
'ashjan': chartDataEmployeeSefer4, fetchEmployee(isCompare: true),
}; fetchEditorCalls(isCompare: true),
]);
}
employeeNames.forEach((name, chartData) { isLoading = false;
var spots = <FlSpot>[]; update();
for (int day = 1; day <= today; day++) { }
spots.add(FlSpot(
day.toDouble(),
employeeDataMap[name]
?.firstWhere(
(e) => e.day == day,
orElse: () => MonthlyEmployeeData(
day: day,
totalEmployees: 0,
name: name,
),
)
.totalEmployees
.toDouble() ??
0,
));
}
// Explicitly cast to List<FlSpot> // ... (Existing Functions _generateSpots, fetchPassengers, etc.) ...
if (name == 'maryam') List<FlSpot> _generateSpots(List<dynamic> data, String dateKey,
chartDataEmployeeMaryam = List<FlSpot>.from(spots); String valueKey, DateTime startOfRange) {
if (name == 'yasmine') List<FlSpot> spots = [];
chartDataEmployeeRawda = List<FlSpot>.from(spots); Map<String, double> dataMap = {};
if (name == 'mena') chartDataEmployeeMena = List<FlSpot>.from(spots); for (var item in data) {
if (name == 'ashjan') String dateStr = item[dateKey].toString();
chartDataEmployeeSefer4 = List<FlSpot>.from(spots); double val = double.tryParse(item[valueKey].toString()) ?? 0.0;
}); dataMap[dateStr] = val;
} catch (e) { }
Log.print('Error in fetchEmployee: $e'); DateTime rangeEnd =
// Set empty FlSpot lists in case of error (startOfRange == startDate) ? endDate! : compareEndDate!;
chartDataEmployeeMaryam = <FlSpot>[]; int totalDays = rangeEnd.difference(startOfRange).inDays + 1;
chartDataEmployeeRawda = <FlSpot>[]; for (int i = 0; i < totalDays; i++) {
chartDataEmployeeMena = <FlSpot>[]; DateTime currentDate = startOfRange.add(Duration(days: i));
chartDataEmployeeSefer4 = <FlSpot>[]; String dateKeyStr = DateFormat('yyyy-MM-dd').format(currentDate);
totalMonthlyRides = '0'; double value = dataMap[dateKeyStr] ?? 0.0;
} finally { spots.add(FlSpot((i + 1).toDouble(), value));
isLoading = false; }
update(); 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'];
if (!isCompare &&
jsonData.isNotEmpty &&
jsonData[0]['totalMonthly'] != null) {
totalMonthlyPassengers = jsonData[0]['totalMonthly'].toString();
}
List<FlSpot> spots =
_generateSpots(jsonData, 'day', 'totalPassengers', start);
if (isCompare)
chartDataPassengersCompare = spots;
else
chartDataPassengers = spots;
}
Future<void> fetchRides({bool isCompare = false}) async {
DateTime start = isCompare ? compareStartDate! : startDate!;
DateTime end = isCompare ? compareEndDate! : endDate!;
var res = await CRUD()
.get(link: AppLink.getRidesStatic, payload: _getPayload(start, end));
var jsonResponse = jsonDecode(res);
if (jsonResponse['status'] == 'failure') return;
final List<dynamic> jsonData = jsonResponse['message'];
if (!isCompare &&
jsonData.isNotEmpty &&
jsonData[0]['totalMonthly'] != null) {
totalMonthlyRides = jsonData[0]['totalMonthly'].toString();
}
List<FlSpot> spots = _generateSpots(jsonData, 'day', 'totalRides', start);
if (isCompare)
chartDataRidesCompare = spots;
else
chartDataRides = spots;
}
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'];
if (!isCompare &&
jsonData.isNotEmpty &&
jsonData[0]['totalMonthlyDrivers'] != null) {
totalMonthlyDrivers = jsonData[0]['totalMonthlyDrivers'].toString();
}
if (!isCompare) {
staticList = jsonData;
}
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;
} }
} }
Future<void> fetchDrivers() async { Future<void> fetchEmployee({bool isCompare = false}) async {
isLoading = true; try {
update(); // Notify the observers about the loading state change DateTime start = isCompare ? compareStartDate! : startDate!;
DateTime end = isCompare ? compareEndDate! : endDate!;
var res = await CRUD().get(
link: AppLink.getEmployeeStatic, payload: _getPayload(start, end));
var res = await CRUD().get( if (isCompare) {
link: AppLink.getdriverstotalMonthly, chartDataEmployeerama1Compare = [];
payload: {}, chartDataEmployeeshahdCompare = [];
); chartDataEmployeeRama2Compare = [];
jsonData2 = jsonDecode(res); chartDataEmployeeSefer4Compare = [];
var jsonResponse = jsonDecode(res) as Map<String, dynamic>; } else {
isLoading = false; chartDataEmployeerama1 = [];
final List<dynamic> jsonData = jsonResponse['message']; chartDataEmployeeshahd = [];
staticList = jsonData; chartDataEmployeeRama2 = [];
totalMonthlyDrivers = jsonData[0]['totalDrivers'].toString(); chartDataEmployeeSefer4 = [];
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 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': chartDataEmployeerama1Compare,
'shahd': chartDataEmployeeshahdCompare,
'rama2': chartDataEmployeeRama2Compare,
'mayar': chartDataEmployeeSefer4Compare
}
: {
'rama1': chartDataEmployeerama1,
'shahd': chartDataEmployeeshahd,
'rama2': chartDataEmployeeRama2,
'mayar': chartDataEmployeeSefer4
};
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') {
// 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 fetchEmployee: $e');
}
} }
Future getAll() async { Future<void> fetchEditorCalls({bool isCompare = false}) async {
await fetch(); try {
await fetchRides(); DateTime start = isCompare ? compareStartDate! : startDate!;
await fetchDrivers(); DateTime end = isCompare ? compareEndDate! : endDate!;
await fetchEmployee();
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 {
isLoadingNotes = false;
update();
}
} }
} }

View File

@@ -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('حدث خطأ من الخادم. حاول مجددًا.');

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',
); // );
} // }
} // }
Get.back(); NotificationService.sendNotification(
} else { target: 'passengers', // الإرسال لجميع المشتركين في "service"
// Handle the case where 'data' is not a list title: title.text,
print('Data is not a list: $tokensPassengersData'); body: body.text,
} isTopic: true,
category: 'fromAdmin', // فئة توضح نوع الإشعار
);
Get.back();
// } else {
// // Handle the case where 'data' is not a list
// print('Data is not a list: $tokensPassengersData');
// }
}), }),
cancel: MyElevatedButton( cancel: MyElevatedButton(
title: 'cancel', title: 'cancel',

View 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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,180 +1,237 @@
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());
} }
}, },
{
'title': 'إرسال واتساب للسائقين', // هذا هو الجزء الذي طلبته تحديداً
'icon': Icons.message_outlined, if (isSuperAdmin)
'iconColor': Colors.green.shade600, {
'onPressed': () => _showWhatsAppDialog(context) 'title': 'واتساب سائقين',
}, 'icon': Icons.message_rounded,
{ 'color': Colors.green,
'title': 'إرسال إشعار للسائقين', 'onPressed': () => _showWhatsAppDialog(context)
'icon': Icons.notifications_active_outlined, },
'onPressed': () async =>
await Get.put(NotificationController()).getTokensDrivers() // --- عودة للعناصر العامة (أو يمكنك تقييدها أيضاً) ---
},
{ if (isSuperAdmin)
'title': 'إرسال إشعار للركاب', {
'icon': Icons.notification_important_outlined, 'title': 'إشعار للسائقين',
'onPressed': () async => 'icon': Icons.notifications_active_rounded,
await Get.put(NotificationController()).getTokensPassengers() 'color': Colors.deepOrange,
}, 'onPressed': () async =>
{ await Get.put(NotificationController()).sendNotificationDrivers()
'title': 'تسجيل كابتن جديد', },
'icon': Icons.person_add_alt_1_outlined, if (isSuperAdmin)
'onPressed': () async { {
await Get.put(RegisterCaptainController()) 'title': 'إشعار للركاب',
.getDriverNotCompleteRegistration(); 'icon': Icons.notification_important_rounded,
Get.to(() => const DriversCantRegister()); 'color': Colors.pinkAccent,
} 'onPressed': () async => await Get.put(NotificationController())
}, .sendNotificationPassengers()
{ },
'title': 'تحديث الباقات', if (isSuperAdmin)
'icon': Icons.inventory_2_outlined, {
'onPressed': () => Get.to(() => PackageUpdateScreen()) 'title': 'تسجيل كابتن',
}, 'icon': Icons.person_add_alt_1_rounded,
{ 'color': Colors.cyan,
'title': 'الموظفون', 'onPressed': () => Get.to(() => DriversPendingPage())
'icon': Icons.badge_outlined, },
'onPressed': () => Get.to(() => EmployeePage())
}, if (isSuperAdmin) // تحديث الباقات للمدير فقط
{ {
'title': 'أفضل السائقين', 'title': 'تحديث الباقات',
'icon': Icons.star_border_purple500_outlined, 'icon': Icons.inventory_2_rounded,
'onPressed': () => Get.to(() => DriverTheBest()) 'color': Colors.brown,
}, 'onPressed': () => Get.to(() => PackageUpdateScreen())
{ },
'title': 'إضافة فاتورة',
'icon': Icons.post_add_outlined, if (isSuperAdmin) // الموظفون للمدير فقط
// 'onPressed': () => Get.to(() => AddInvoicePage()) {
'onPressed': () => Get.to(() => InvoiceListPage()) 'title': 'الموظفون',
}, 'icon': Icons.badge_rounded,
{ 'color': Colors.blueGrey,
'title': 'إضافة جهاز كمسؤول', 'onPressed': () => Get.to(() => EmployeePage())
'icon': Icons.admin_panel_settings_outlined, },
'onPressed': () async => await CRUD()
.post(link: AppLink.addAdminUser, payload: {'name': 'b'}) if (isSuperAdmin)
}, {
'title': 'أفضل السائقين',
'icon': Icons.star_rounded,
'color': Colors.amber,
'onPressed': () => Get.to(() => DriverTheBestRedesigned())
},
if (isSuperAdmin) // الفواتير للمدير فقط
{
'title': 'الفواتير',
'icon': Icons.receipt_long_rounded,
'color': Colors.green.shade800,
'onPressed': () => Get.to(() => InvoiceListPage())
},
if (isSuperAdmin)
{
'title': 'سجل الأخطاء',
'icon': Icons.error_outline_rounded,
'color': Colors.redAccent,
'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 = [
{ // يمكنك تطبيق نفس المنطق هنا لإخفاء الأرباح عن الموظفين العاديين
'title': 'رصيد الرسائل', if (isSuperAdmin)
'value': controller.creditSMS.toString(), {
'icon': Icons.sms_outlined, 'title': 'رصيد الرسائل',
'color': Colors.lightBlue 'value': controller.creditSMS.toString(),
}, 'icon': Icons.sms_outlined,
'color': Colors.lightBlue
},
{ {
'title': 'الركاب', 'title': 'الركاب',
'value': data['countPassengers'].toString(), 'value': data['countPassengers'].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
}, },
{
'title': 'متوسط التكلفة', if (isSuperAdmin) // إخفاء الأمور المالية
'value': _formatCurrency(data['avg_passenger_price']), {
'icon': Icons.monetization_on_outlined, 'title': 'متوسط التكلفة',
'color': Colors.green 'value': _formatCurrency(data['avg_passenger_price']),
}, 'icon': Icons.monetization_on_outlined,
'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(
children: [ expandedHeight: 120.0,
// --- Statistics Grid Section --- floating: true,
AnimationLimiter( pinned: true,
child: GridView.builder( backgroundColor: AppColor.primaryColor,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( elevation: 0,
crossAxisCount: _calculateCrossAxisCount(context), flexibleSpace: FlexibleSpaceBar(
mainAxisSpacing: 12.0, titlePadding:
crossAxisSpacing: 12.0, const EdgeInsets.only(left: 16, right: 16, bottom: 16),
childAspectRatio: 1.8, 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),
],
),
), ),
itemCount: statCards.length, child: Stack(
shrinkWrap: true, children: [
physics: const NeverScrollableScrollPhysics(), Positioned(
itemBuilder: (context, index) { left: -20,
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(
crossAxisCount: _calculateCrossAxisCount(context),
mainAxisSpacing: 12.0,
crossAxisSpacing: 12.0,
childAspectRatio: 1.6,
),
delegate: SliverChildBuilderDelegate(
(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),
),
],
),
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: [
isleading: false, 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)),
controller: _messageController, content: Column(
label: 'الرسالة', mainAxisSize: MainAxisSize.min,
hint: 'أدخل نص الرسالة هنا', children: [
type: TextInputType.text, 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,
label: 'الرسالة',
hint: 'اكتب الرسالة هنا...',
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,
),
],
),
),
),
),
); );
} }
} }

View File

@@ -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,
body: [
GetBuilder<CaptainAdminController>(
builder: (captainAdminController) => Column(
children: [
captainAdminController.isLoading
? const MyCircularProgressIndicator()
: Column(
children: [
Padding(
padding: const EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
captainAdmin(
captainAdminController,
'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(
'Add Points to their wallet as prize'
.tr,
style: AppStyle.title,
),
Form(
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, isleading: true,
body: [
Container(
height: MediaQuery.of(context).size.height, // لضمان أخذ المساحة
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// --- شريط البحث المحسن ---
_buildSearchSection(context),
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),
],
),
),
],
); );
} }
Container captainAdmin(CaptainAdminController captainAdminController, // --- ودجت البحث ---
String title, String jsonField) { Widget _buildSearchSection(BuildContext context) {
return Container( return Container(
height: Get.height * .1, decoration: BoxDecoration(
decoration: BoxDecoration(border: Border.all(width: 2)), color: Colors.white,
child: Padding( borderRadius: BorderRadius.circular(15),
padding: const EdgeInsets.all(8.0), boxShadow: [
child: GestureDetector( BoxShadow(
onTap: () {}, color: Colors.grey.withOpacity(0.1),
child: Column( spreadRadius: 2,
children: [ blurRadius: 10,
Text( offset: const Offset(0, 2),
title.tr, ),
style: AppStyle.title, ],
), ),
Text( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
captainAdminController.captainData['message'][0][jsonField] child: Row(
.toString(), children: [
style: AppStyle.title, Expanded(
), 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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off_rounded, size: 80, color: Colors.grey[300]),
const SizedBox(height: 10),
Text(
'No captains found.'.tr,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey[500],
),
),
],
),
);
}
} }

View File

@@ -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: [
Padding( SingleChildScrollView(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.only(bottom: 40),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( // --- Header Section (Avatar & Name) ---
'Email is ${data['email']}', _buildHeaderSection(context, data),
style: AppStyle.title,
), const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
Text( child: Column(
'Phone is ${data['phone']}', children: [
style: AppStyle.title, // --- Personal Information Card ---
), _buildInfoCard(
Text( title: 'Personal Information',
'gender is ${data['gender']}', icon: Icons.person,
style: AppStyle.title, children: [
), _buildDetailTile(
], Icons.email_outlined,
), 'Email',
Row( isSuperAdmin
mainAxisAlignment: MainAxisAlignment.spaceBetween, ? data['email']
children: [ : _maskEmail(
Text( data['email']) // Mask email for non-super
'status is ${data['status']}', ),
style: AppStyle.title, _buildDetailTile(
), Icons.phone_iphone,
Text( 'Phone',
'birthdate is ${data['birthdate']}', _formatPhoneNumber(
style: AppStyle.title, data['phone'].toString(), isSuperAdmin)),
), _buildDetailTile(Icons.transgender, 'Gender',
], data['gender'] ?? 'Not specified'),
), _buildDetailTile(Icons.cake_outlined, 'Birthdate',
Row( data['birthdate'] ?? 'N/A'),
mainAxisAlignment: MainAxisAlignment.spaceBetween, ],
children: [ ),
Text( const SizedBox(height: 16),
'site is ${data['site']}',
style: AppStyle.title, // --- Ride Statistics Card ---
), _buildInfoCard(
// Text( title: 'Performance & Stats',
// 'sosPhone is ${data['sosPhone']}', icon: Icons.bar_chart_rounded,
// style: AppStyle.title, children: [
// ), _buildDetailTile(Icons.star_rate_rounded, 'Rating',
], '${data['ratingPassenger'] ?? 0.0} / 5.0',
), valueColor: Colors.amber[700]),
Row( _buildDetailTile(Icons.directions_car_filled_outlined,
mainAxisAlignment: MainAxisAlignment.spaceBetween, 'Total Rides', data['countPassengerRide']),
children: [ _buildDetailTile(Icons.cancel_outlined,
Text( 'Canceled Rides', data['countPassengerCancel'],
'Count Feedback is ${data['countFeedback']}', valueColor: Colors.redAccent),
style: AppStyle.title, ],
), ),
Text( const SizedBox(height: 30),
'Count Driver Rate is ${data['countDriverRate']}',
style: AppStyle.title, // --- Action Buttons ---
), _buildActionButtons(
], context, controller, data, isSuperAdmin),
), ],
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(
decoration: BoxDecoration(
border: Border.all(width: 3, color: AppColor.yellowColor)),
child: TextButton(
onPressed: () async {
Get.defaultDialog(
title: 'Send Notification'.tr,
titleStyle: AppStyle.title,
content: Form(
key: key,
child: Column(
children: [
MyTextForm(
controller: titleNotify,
label: 'title'.tr,
hint: 'title notificaton'.tr,
type: TextInputType.name),
const SizedBox(
height: 10,
),
MyTextForm(
controller: bodyNotify,
label: 'body'.tr,
hint: 'body notificaton'.tr,
type: TextInputType.name)
],
),
),
confirm: MyElevatedButton(
title: 'Send',
onPressed: () {
if (key.currentState!.validate()) {
FirebaseMessagesController()
.sendNotificationToAnyWithoutData(
titleNotify.text,
bodyNotify.text,
data['passengerToken'],
'order.wav');
Get.back();
}
}));
},
child: Text(
"Send Notificaion to Captains ".tr,
style: AppStyle.title,
),
), ),
) ),
], ],
), ),
) ),
], ],
isleading: true, );
}
// --- 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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: AppColor.primaryColor, size: 22),
const SizedBox(width: 10),
Text(title.tr,
style: const TextStyle(
fontSize: 17, fontWeight: FontWeight.bold)),
],
),
Divider(height: 25, color: Colors.grey.withOpacity(0.2)),
...children,
],
),
);
}
Widget _buildDetailTile(IconData icon, String label, dynamic value,
{Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8)),
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(
title: 'Send Notification'.tr,
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
content: Form(
key: controller.formCaptainPrizeKey,
child: Column(
children: [
MyTextForm(
controller: controller.titleNotify,
label: 'Title'.tr,
hint: 'Enter notification title'.tr,
type: TextInputType.text,
),
const SizedBox(height: 10),
MyTextForm(
controller: controller.bodyNotify,
label: 'Body'.tr,
hint: 'Enter message body'.tr,
type: TextInputType.text,
),
],
),
),
confirm: SizedBox(
width: 100,
child: MyElevatedButton(
title: 'Send',
onPressed: () {
// Check if key is valid (might be recreated)
if (controller.formCaptainPrizeKey.currentState?.validate() ??
true) {
FirebaseMessagesController().sendNotificationToAnyWithoutData(
controller.titleNotify.text,
controller.bodyNotify.text,
data['passengerToken'] ?? '', // Safety check
'order.wav');
Get.back();
Get.snackbar("Success", "Notification Sent",
backgroundColor: Colors.green.withOpacity(0.2));
}
},
),
),
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(),
child: Text('Cancel'.tr, style: const TextStyle(color: Colors.grey))),
); );
} }
} }

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

View File

@@ -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(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Container(
decoration:
const BoxDecoration(color: AppColor.secondaryColor),
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( @override
title: controller.captain['message'][0] State<FormCaptain> createState() => _FormCaptainState();
['email'], }
titleStyle: AppStyle.title,
content: Column( class _FormCaptainState extends State<FormCaptain> {
mainAxisAlignment: final CaptainAdminController controller = Get.find();
MainAxisAlignment.spaceBetween, final _formKey = GlobalKey<FormState>();
children: [
Text( late TextEditingController firstNameController;
'Name is ${controller.captain['message'][0]['first_name']} ${controller.captain['message'][0]['last_name']}', late TextEditingController lastNameController;
style: AppStyle.title, late TextEditingController phoneController;
),
Text( bool isEditMode = false;
'phone is ${controller.captain['message'][0]['phone']}', Map<String, dynamic>? captainData;
style: AppStyle.title,
), @override
], void initState() {
), super.initState();
confirm: MyElevatedButton( if (Get.arguments != null && Get.arguments['isEditMode'] == true) {
title: 'Go To Details'.tr, isEditMode = true;
onPressed: () { captainData = Get.arguments['captainData'];
Get.to( firstNameController =
() => const CaptainsDetailsPage(), TextEditingController(text: captainData?['first_name'] ?? '');
arguments: { lastNameController =
'data': controller TextEditingController(text: captainData?['last_name'] ?? '');
.captain['message'][0], phoneController =
}); TextEditingController(text: captainData?['phone'] ?? '');
})); } else {
} firstNameController = TextEditingController();
}, lastNameController = TextEditingController();
child: const Icon(Icons.search)), phoneController = TextEditingController();
hintText: 'Search for Passenger'.tr, }
hintStyle: AppStyle.title, }
hintMaxLines: 1,
prefixIcon: IconButton( @override
onPressed: () async { void dispose() {
controller.captainController.clear(); firstNameController.dispose();
// controller.clearPlaces(); lastNameController.dispose();
}, phoneController.dispose();
icon: Icon( super.dispose();
Icons.clear, }
color: Colors.red[300],
), Future<void> _saveForm() async {
), if (_formKey.currentState!.validate()) {
), // Create a map of the updated data
controller: controller.captainController, 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: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
MyTextForm(
controller: firstNameController,
label: 'First Name'.tr,
hint: 'Enter first name'.tr,
type: TextInputType.name,
),
const SizedBox(height: 16),
MyTextForm(
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,
),
],
),
),
),
],
);
}
} }

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

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

View File

@@ -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: [ children: [
Row( Column(
mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyElevatedButton( Row(
title: 'Giza', children: [
onPressed: () { const Icon(Icons.local_taxi,
Get.to(() => DriverTheBestGiza()); color: Colors.yellow, size: 24),
}), const SizedBox(width: 8),
MyElevatedButton( Text(
title: 'Alexandria', 'Best Drivers Dashboard'.tr,
onPressed: () { style: const TextStyle(
Get.to(() => DriverTheBestAlexandria()); color: Colors.white,
}), fontSize: 20,
], fontWeight: FontWeight.bold,
),
SizedBox(
height: Get.height * .7,
child: ListView.builder(
itemCount: driverthebest.driver.length,
itemBuilder: (context, index) {
final driver = driverthebest.driver[index];
return ListTile(
leading: CircleAvatar(
child: Text(
(int.parse(driver['driver_count']) * 5 / 3600)
.toStringAsFixed(
0), // Perform division first, then convert to string
), ),
), ),
title: ],
Text((driver['name_arabic']) ?? 'Unknown Name'), ),
subtitle: const SizedBox(height: 4),
Text('Phone: ${(driver['phone']) ?? 'N/A'}'), Row(
trailing: IconButton( children: [
onPressed: () async { const Icon(Icons.access_time,
Get.defaultDialog( color: Colors.grey, size: 12),
title: const SizedBox(width: 4),
'are you sure to pay to this driver gift' Text(
.tr, ctrl.lastUpdated.isNotEmpty
middleText: '', ? 'Updated: ${ctrl.lastUpdated}'
onConfirm: () async { : 'Data Live',
final wallet = style: TextStyle(
Get.put(WalletController()); color: Colors.grey[400], fontSize: 12),
await wallet.addPaymentToDriver(
'200',
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();
},
onCancel: () => Get.back());
},
icon: const Icon(Icons.wallet_giftcard_rounded),
), ),
); ],
}, ),
],
),
// Action Buttons (Delete Box & Refresh)
Row(
children: [
// Delete Box Icon
IconButton(
onPressed: () {
Get.defaultDialog(
title: "Reset Paid Status",
middleText:
"Are you sure you want to clear the list of paid drivers? This cannot be undone.",
textConfirm: "Yes, Clear",
textCancel: "Cancel",
confirmTextColor: Colors.white,
buttonColor: Colors.red,
onConfirm: () {
ctrl.clearPaidStorage();
Get.back();
},
);
},
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]),
), ),
], ],
) ),
: const Center( ),
child: Text('No drivers available.'),
); // Status Badge
}) Container(
], padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
isleading: true, 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)),
),
),
],
)
],
),
);
}
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(),
); );
} }
} }

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

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

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

View File

@@ -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) {
children: [ return const Center(child: MyCircularProgressIndicator());
Padding( }
padding: const EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
passengerAdmin(
passengerAdminController,
'Passengers Count',
'countPassenger',
),
MyElevatedButton(
title: 'Add Prize to Gold Passengers',
onPressed: () {
var date = DateTime.now();
var day = date.weekday;
if (day == 6) { return Column(
// Saturday is 6 children: [
Get.defaultDialog( // --- قسم الإحصائيات والجوائز (Dashboard) ---
title: Padding(
'Add Prize to Gold Passengers', padding: const EdgeInsets.all(16.0),
titleStyle: AppStyle.title, child: _buildDashboardCard(context, controller),
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();
}));
}
})
],
),
),
const SizedBox(
height: 10,
),
formSearchPassengers(),
SizedBox(
height: Get.height * .5,
child: ListView.builder(
itemCount: passengerAdminController
.passengersData['message'].length,
itemBuilder: (context, index) {
final user = passengerAdminController
.passengersData['message'][index];
return InkWell( // --- عنوان القائمة ---
onTap: () { Padding(
Get.to(const PassengerDetailsPage(), padding: const EdgeInsets.symmetric(horizontal: 20.0),
arguments: { child: Row(
'data': user, mainAxisAlignment: MainAxisAlignment.spaceBetween,
}); children: [
}, Text(
child: Padding( "All Passengers".tr,
padding: const EdgeInsets.all(3), style: AppStyle.title.copyWith(
child: Container( fontSize: 18, fontWeight: FontWeight.bold),
decoration: BoxDecoration( ),
border: Border.all(width: 2)), Text(
child: ListTile( "${controller.passengersData['message']?.length ?? 0} Users",
title: Row( style:
mainAxisAlignment: TextStyle(color: Colors.grey[600], fontSize: 12),
MainAxisAlignment ),
.spaceBetween, ],
children: [ ),
Text( ),
'Name : ${(user['first_name'])} ${(user['last_name'])}', const SizedBox(height: 10),
style: AppStyle.title,
), // --- قائمة الركاب ---
Text( // استخدام Expanded هنا هو الحل الجذري لمكلة Overflow
'Rating : ${user['ratingPassenger']}', Expanded(
style: AppStyle.title, child: _buildPassengersList(controller, isSuperAdmin),
), ),
],
), // مساحة سفلية صغيرة لضمان عدم التصاق القائمة بالحافة
subtitle: Row( const SizedBox(height: 20),
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, // --- تصميم بطاقة الإحصائيات (Dashboard) ---
String title, String jsonField) { 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( return Container(
height: Get.height * .1, padding: const EdgeInsets.all(20),
decoration: BoxDecoration(border: Border.all(width: 2)), decoration: BoxDecoration(
child: Padding( color: Colors.white,
padding: const EdgeInsets.all(8.0), borderRadius: BorderRadius.circular(20),
child: GestureDetector( boxShadow: [
onTap: () {}, BoxShadow(
child: Column( color: Colors.grey.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
Row(
children: [ children: [
Text( Container(
title.tr, padding: const EdgeInsets.all(12),
style: AppStyle.title, decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(Icons.groups_rounded,
color: AppColor.primaryColor, size: 30),
), ),
Text( const SizedBox(width: 15),
passengerAdminController.passengersData['message'][0][jsonField] Column(
.toString(), crossAxisAlignment: CrossAxisAlignment.start,
style: AppStyle.title, 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(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_off_outlined, size: 60, color: Colors.grey[300]),
const SizedBox(height: 10),
Text("No passengers found".tr,
style: TextStyle(color: Colors.grey[400])),
],
),
);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
itemCount: passengers.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final user = passengers[index];
return _buildPassengerItem(user, isSuperAdmin);
},
);
}
// --- عنصر الراكب الواحد (Card) ---
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(
decoration: BoxDecoration(
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(
padding: const EdgeInsets.all(12.0),
child: Row(
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(
children: [
Text(
'Add Points to Gold Passengers wallet'.tr,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
const SizedBox(height: 20),
MyTextForm(
controller: controller.passengerPrizeController,
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),
);
}
}
} }

View File

@@ -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: [
Padding( SingleChildScrollView(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.only(bottom: 40),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( // --- Header Section (Avatar & Name) ---
'Email is ${data['email']}', _buildHeaderSection(context, data),
style: AppStyle.title,
), const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 16.0),
Text( child: Column(
'Phone is ${data['phone']}', children: [
style: AppStyle.title, // --- Personal Information Card ---
), _buildInfoCard(
Text( title: 'Personal Information',
'gender is ${data['gender']}', icon: Icons.person,
style: AppStyle.title, children: [
), _buildDetailTile(
], Icons.email_outlined,
), 'Email',
Row( isSuperAdmin
mainAxisAlignment: MainAxisAlignment.spaceBetween, ? data['email']
children: [ : _maskEmail(data['email']),
Text(
'status is ${data['status']}',
style: AppStyle.title,
),
Text(
'birthdate is ${data['birthdate']}',
style: AppStyle.title,
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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(
decoration: BoxDecoration(
border: Border.all(width: 3, color: AppColor.yellowColor)),
child: TextButton(
onPressed: () async {
Get.defaultDialog(
title: 'Send Notification'.tr,
titleStyle: AppStyle.title,
content: Form(
key: key,
child: Column(
children: [
MyTextForm(
controller: titleNotify,
label: 'title'.tr,
hint: 'title notificaton'.tr,
type: TextInputType.name),
const SizedBox(
height: 10,
),
MyTextForm(
controller: bodyNotify,
label: 'body'.tr,
hint: 'body notificaton'.tr,
type: TextInputType.name)
],
),
), ),
confirm: MyElevatedButton( _buildDetailTile(
title: 'Send', Icons.phone_iphone,
onPressed: () { 'Phone',
if (key.currentState!.validate()) { _formatPhoneNumber(
FirebaseMessagesController() data['phone'].toString(), isSuperAdmin),
.sendNotificationToAnyWithoutData( ),
titleNotify.text, _buildDetailTile(
bodyNotify.text, Icons.transgender,
data['passengerToken'], 'Gender',
'order.wav'); data['gender'] ?? 'Not specified',
Get.back(); ),
} _buildDetailTile(
})); Icons.cake_outlined,
}, 'Birthdate',
child: Text( data['birthdate'] ?? 'N/A',
"Send Notificaion to Passenger ".tr, ),
style: AppStyle.title, _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),
],
), ),
) ),
], ],
), ),
) ),
], ],
isleading: true, );
}
// --- 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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: AppColor.primaryColor, size: 22),
const SizedBox(width: 10),
Text(title.tr,
style: const TextStyle(
fontSize: 17, fontWeight: FontWeight.bold)),
],
),
Divider(height: 25, color: Colors.grey.withOpacity(0.2)),
...children,
],
),
);
}
Widget _buildDetailTile(IconData icon, String label, dynamic value,
{Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8)),
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(
title: 'Send Notification'.tr,
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
content: Form(
key: controller.formPrizeKey,
child: Column(
children: [
MyTextForm(
controller: controller.titleNotify,
label: 'Title'.tr,
hint: 'Notification title'.tr,
type: TextInputType.text),
const SizedBox(height: 10),
MyTextForm(
controller: controller.bodyNotify,
label: 'Body'.tr,
hint: 'Message body'.tr,
type: TextInputType.text)
],
),
),
confirm: SizedBox(
width: 100,
child: MyElevatedButton(
title: 'Send',
onPressed: () {
// Validate form safely
if (controller.formPrizeKey.currentState?.validate() ?? false) {
FirebaseMessagesController().sendNotificationToAnyWithoutData(
controller.titleNotify.text,
controller.bodyNotify.text,
data['passengerToken'],
'order.wav');
Get.back();
Get.snackbar('Success', 'Notification sent successfully!',
backgroundColor: Colors.green.withOpacity(0.2));
}
},
),
),
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)),
),
); );
} }
} }

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-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.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 />
</dict> </dict>
</plist> </plist>

View File

@@ -1,32 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-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>CFBundleDevelopmentRegion</key> <key>com.apple.security.network.client</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <true />
<key>CFBundleExecutable</key> <key>CFBundleDevelopmentRegion</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleIconFile</key> <key>CFBundleExecutable</key>
<string></string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIconFile</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string></string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleIdentifier</key>
<string>6.0</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key> <key>CFBundleInfoDictionaryVersion</key>
<string>$(PRODUCT_NAME)</string> <string>6.0</string>
<key>CFBundlePackageType</key> <key>CFBundleName</key>
<string>APPL</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundleShortVersionString</key> <key>CFBundlePackageType</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>APPL</string>
<key>CFBundleVersion</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>LSMinimumSystemVersion</key> <key>CFBundleVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSHumanReadableCopyright</key> <key>LSMinimumSystemVersion</key>
<string>$(PRODUCT_COPYRIGHT)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSMainNibFile</key> <key>NSHumanReadableCopyright</key>
<string>MainMenu</string> <string>$(PRODUCT_COPYRIGHT)</string>
<key>NSPrincipalClass</key> <key>NSMainNibFile</key>
<string>NSApplication</string> <string>MainMenu</string>
</dict> <key>NSPrincipalClass</key>
</plist> <string>NSApplication</string>
</dict>
</plist>

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-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.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true />
</dict> <key>com.apple.security.network.client</key>
</plist> <true />
</dict>
</plist>

View File

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

View File

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