Initial commit for intaleq_admin
This commit is contained in:
@@ -11,8 +11,9 @@ class AppLink {
|
||||
// static final String endPoint = box.read(BoxName.serverChosen);
|
||||
// static final String server = Env.seferCairoServer;
|
||||
|
||||
static final String server = 'https://intaleq.xyz/intaleq';
|
||||
static String loginJwtDriver = "https://intaleq.xyz/intaleq/loginAdmin.php";
|
||||
static final String server = 'https://api.intaleq.xyz/intaleq';
|
||||
static String loginJwtDriver =
|
||||
"https://api.intaleq.xyz/intaleq/loginAdmin.php";
|
||||
|
||||
static String googleMapsLink = 'https://maps.googleapis.com/maps/api/';
|
||||
static String llama = 'https://api.llama-api.com/chat/completions';
|
||||
@@ -41,6 +42,10 @@ class AppLink {
|
||||
"$seferPaymentServer/Admin/getPaymentsDashboard.php";
|
||||
static String getSeferWallet = "$tripzPaymentServer/seferWallet/get.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 =
|
||||
"$tripzPaymentServer/payment/updatePaymetToPaid.php";
|
||||
static String wallet = '$tripzPaymentServer/passengerWallet';
|
||||
@@ -238,7 +243,26 @@ class AppLink {
|
||||
static String getPassengerDetailsByPassengerID =
|
||||
"$server/Admin/getPassengerDetailsByPassengerID.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 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 getdashbord = "$server/Admin/dashbord.php";
|
||||
static String getEmployee = "$server/Admin/employee/get.php";
|
||||
@@ -262,6 +286,10 @@ class AppLink {
|
||||
static String getPassengersStatic = "$serviceApp/getPassengersStatic.php";
|
||||
static String getRidesStatic = "$serviceApp/getRidesStatic.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 =
|
||||
"$serviceApp/getdriverstotalMonthly.php";
|
||||
|
||||
|
||||
@@ -32,6 +32,42 @@ class CaptainAdminController extends GetxController {
|
||||
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 {
|
||||
String? paymentId;
|
||||
//todo link to add wallet to captain
|
||||
|
||||
@@ -21,41 +21,47 @@ class DashboardController extends GetxController {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
// الطلب من السيرفر الرئيسي
|
||||
// 🔹 Request main dashboard data
|
||||
var res = await CRUD().get(link: AppLink.getdashbord, payload: {});
|
||||
print('📡 Main dashboard response: $res');
|
||||
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res);
|
||||
// Log.print('d: ${d}');
|
||||
dashbord = d['message']; // هذا عبارة عن List<Map>
|
||||
print('✅ Decoded main dashboard: ${jsonEncode(d)}');
|
||||
dashbord = d['message'];
|
||||
} else {
|
||||
print('❌ Failed to load main dashboard');
|
||||
}
|
||||
|
||||
// الطلب من سيرفر المحافظ
|
||||
// 🔹 Request wallet dashboard data
|
||||
var resPayments = await CRUD().postWallet(
|
||||
link: AppLink.getPaymentsDashboard,
|
||||
payload: {},
|
||||
);
|
||||
print('💳 Wallet dashboard response: $resPayments');
|
||||
|
||||
if (resPayments != 'failure') {
|
||||
var p = resPayments;
|
||||
// Log.print('p: ${p}');
|
||||
print('✅ Decoded wallet dashboard: ${jsonEncode(p)}');
|
||||
|
||||
// نتأكد أن الكل Map بداخل List
|
||||
if (dashbord.isNotEmpty &&
|
||||
p['message'] is List &&
|
||||
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(
|
||||
link: 'https://sms.kazumi.me/api/sms/check-credit',
|
||||
payload: {"username": "Sefer", "password": AK.smsPasswordEgypt},
|
||||
);
|
||||
|
||||
creditSMS = res2['credit'];
|
||||
Log.print(' res2[credit]: ${res2['credit']}');
|
||||
Log.print('creditSMS: ${creditSMS}');
|
||||
print('📱 SMS Credit Response: ${jsonEncode(res2)}');
|
||||
print('💰 creditSMS: $creditSMS');
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
@@ -70,7 +76,7 @@ class DashboardController extends GetxController {
|
||||
// box.read(BoxName.tokensDrivers)['message'][i]['phone'].toString(),
|
||||
smsText.text,
|
||||
);
|
||||
// Log.print('CRUD().phoneDriversTest.: ${phoneNumber['phone']}');
|
||||
// print('CRUD().phoneDriversTest.: ${phoneNumber['phone']}');
|
||||
Future.delayed(const Duration(microseconds: 20));
|
||||
}
|
||||
Get.back();
|
||||
|
||||
@@ -43,6 +43,70 @@ class PassengerAdminController extends GetxController {
|
||||
|
||||
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 {
|
||||
try {
|
||||
|
||||
@@ -1,238 +1,507 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../constant/links.dart';
|
||||
import '../../models/model/passengers_model.dart';
|
||||
import '../../print.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class StaticController extends GetxController {
|
||||
Map<String, dynamic> jsonData1 = {};
|
||||
Map<String, dynamic> jsonData2 = {};
|
||||
List staticList = [];
|
||||
var chartDataPassengers;
|
||||
var chartDataDrivers;
|
||||
var chartDataDriversCalling;
|
||||
var chartDataRides;
|
||||
var chartDataEmployee;
|
||||
var chartDataEmployeeMaryam;
|
||||
var chartDataEmployeeRawda;
|
||||
var chartDataEmployeeMena;
|
||||
var chartDataEmployeeSefer4;
|
||||
var chartDataDriversMatchingNotes;
|
||||
// --- Date & State Management ---
|
||||
DateTime? startDate = DateTime(DateTime.now().year, DateTime.now().month, 1);
|
||||
DateTime? endDate =
|
||||
DateTime(DateTime.now().year, DateTime.now().month + 1, 0);
|
||||
|
||||
DateTime? compareStartDate;
|
||||
DateTime? compareEndDate;
|
||||
|
||||
bool isComparing = 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 {
|
||||
isLoading = true;
|
||||
update(); // Notify the observers about the loading state change
|
||||
// --- Daily Notes State ---
|
||||
bool isLoadingNotes = false;
|
||||
List<dynamic> dailyNotesList = [];
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getPassengersStatic,
|
||||
payload: {},
|
||||
);
|
||||
jsonData1 = jsonDecode(res);
|
||||
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;
|
||||
// --- Chart Data (Current Range) ---
|
||||
List<FlSpot> chartDataPassengers = [];
|
||||
List<FlSpot> chartDataDrivers = [];
|
||||
List<FlSpot> chartDataRides = [];
|
||||
List<FlSpot> chartDataDriversMatchingNotes = [];
|
||||
|
||||
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 {
|
||||
isLoading = true;
|
||||
update(); // Notify the observers about the loading state change
|
||||
|
||||
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
|
||||
// --- Helpers for View ---
|
||||
double get daysInPeriod {
|
||||
if (startDate == null || endDate == null) return 31;
|
||||
return endDate!.difference(startDate!).inDays + 1.0;
|
||||
}
|
||||
|
||||
Future<void> fetchEmployee() async {
|
||||
try {
|
||||
isLoading = true;
|
||||
update();
|
||||
String get currentDateString {
|
||||
if (startDate == null || endDate == null) return "";
|
||||
return "${DateFormat('yyyy-MM-dd').format(startDate!)} : ${DateFormat('yyyy-MM-dd').format(endDate!)}";
|
||||
}
|
||||
|
||||
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
|
||||
if (res == 'failure') {
|
||||
throw FormatException('Invalid response: $res');
|
||||
}
|
||||
void _calculateCompareDates() {
|
||||
if (startDate == null || endDate == null) return;
|
||||
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
|
||||
chartDataEmployeeMaryam = <FlSpot>[];
|
||||
chartDataEmployeeRawda = <FlSpot>[];
|
||||
chartDataEmployeeMena = <FlSpot>[];
|
||||
chartDataEmployeeSefer4 = <FlSpot>[];
|
||||
totalMonthlyRides = '0';
|
||||
void _clearComparisonData() {
|
||||
chartDataPassengersCompare.clear();
|
||||
chartDataDriversCompare.clear();
|
||||
chartDataRidesCompare.clear();
|
||||
chartDataDriversMatchingNotesCompare.clear();
|
||||
|
||||
// Check for error response
|
||||
if (jsonResponse['status'] == 'failure') {
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
chartDataEmployeerama1Compare.clear();
|
||||
chartDataEmployeeshahdCompare.clear();
|
||||
chartDataEmployeeRama2Compare.clear();
|
||||
chartDataEmployeeSefer4Compare.clear();
|
||||
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
if (jsonData.isEmpty) {
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
chartDataCallsrama1Compare.clear();
|
||||
chartDataCallsShahdCompare.clear();
|
||||
chartDataCallsRama2Compare.clear();
|
||||
chartDataCallsSefer4Compare.clear();
|
||||
}
|
||||
|
||||
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
|
||||
Map<String, List<MonthlyEmployeeData>> employeeDataMap = {};
|
||||
// --- Main Fetch Logic ---
|
||||
Future getAll() async {
|
||||
if (startDate == null || endDate == null) return;
|
||||
|
||||
for (var item in jsonData) {
|
||||
var employeeData = MonthlyEmployeeData.fromJson(item);
|
||||
if (!employeeDataMap.containsKey(employeeData.name)) {
|
||||
employeeDataMap[employeeData.name] = [];
|
||||
}
|
||||
employeeDataMap[employeeData.name]!.add(employeeData);
|
||||
}
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
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
|
||||
final employeeNames = {
|
||||
'maryam': chartDataEmployeeMaryam,
|
||||
'yasmine': chartDataEmployeeRawda,
|
||||
'mena': chartDataEmployeeMena,
|
||||
'ashjan': chartDataEmployeeSefer4,
|
||||
};
|
||||
if (isComparing && compareStartDate != null && compareEndDate != null) {
|
||||
await Future.wait([
|
||||
fetchPassengers(isCompare: true),
|
||||
fetchRides(isCompare: true),
|
||||
fetchDrivers(isCompare: true),
|
||||
fetchEmployee(isCompare: true),
|
||||
fetchEditorCalls(isCompare: true),
|
||||
]);
|
||||
}
|
||||
|
||||
employeeNames.forEach((name, chartData) {
|
||||
var spots = <FlSpot>[];
|
||||
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,
|
||||
));
|
||||
}
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// Explicitly cast to List<FlSpot>
|
||||
if (name == 'maryam')
|
||||
chartDataEmployeeMaryam = List<FlSpot>.from(spots);
|
||||
if (name == 'yasmine')
|
||||
chartDataEmployeeRawda = List<FlSpot>.from(spots);
|
||||
if (name == 'mena') chartDataEmployeeMena = List<FlSpot>.from(spots);
|
||||
if (name == 'ashjan')
|
||||
chartDataEmployeeSefer4 = List<FlSpot>.from(spots);
|
||||
});
|
||||
} catch (e) {
|
||||
Log.print('Error in fetchEmployee: $e');
|
||||
// Set empty FlSpot lists in case of error
|
||||
chartDataEmployeeMaryam = <FlSpot>[];
|
||||
chartDataEmployeeRawda = <FlSpot>[];
|
||||
chartDataEmployeeMena = <FlSpot>[];
|
||||
chartDataEmployeeSefer4 = <FlSpot>[];
|
||||
totalMonthlyRides = '0';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
// ... (Existing Functions _generateSpots, fetchPassengers, etc.) ...
|
||||
List<FlSpot> _generateSpots(List<dynamic> data, String dateKey,
|
||||
String valueKey, DateTime startOfRange) {
|
||||
List<FlSpot> spots = [];
|
||||
Map<String, double> dataMap = {};
|
||||
for (var item in data) {
|
||||
String dateStr = item[dateKey].toString();
|
||||
double val = double.tryParse(item[valueKey].toString()) ?? 0.0;
|
||||
dataMap[dateStr] = val;
|
||||
}
|
||||
DateTime rangeEnd =
|
||||
(startOfRange == startDate) ? endDate! : compareEndDate!;
|
||||
int totalDays = rangeEnd.difference(startOfRange).inDays + 1;
|
||||
for (int i = 0; i < totalDays; i++) {
|
||||
DateTime currentDate = startOfRange.add(Duration(days: i));
|
||||
String dateKeyStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||
double value = dataMap[dateKeyStr] ?? 0.0;
|
||||
spots.add(FlSpot((i + 1).toDouble(), value));
|
||||
}
|
||||
return spots;
|
||||
}
|
||||
|
||||
Future<void> fetchPassengers({bool isCompare = false}) async {
|
||||
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getPassengersStatic, payload: _getPayload(start, end));
|
||||
var jsonResponse = jsonDecode(res);
|
||||
if (jsonResponse['status'] == 'failure') return;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
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 {
|
||||
isLoading = true;
|
||||
update(); // Notify the observers about the loading state change
|
||||
Future<void> fetchEmployee({bool isCompare = false}) async {
|
||||
try {
|
||||
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(
|
||||
link: AppLink.getdriverstotalMonthly,
|
||||
payload: {},
|
||||
);
|
||||
jsonData2 = jsonDecode(res);
|
||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||
isLoading = false;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
staticList = jsonData;
|
||||
totalMonthlyDrivers = jsonData[0]['totalDrivers'].toString();
|
||||
driversData = jsonData.map<MonthlyDriverInstall>((item) {
|
||||
return MonthlyDriverInstall.fromJson(item);
|
||||
}).toList();
|
||||
final List<FlSpot> spots = driversData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.dailyTotalDrivers.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartDataDrivers = spots;
|
||||
final List<FlSpot> spotsCalling = driversData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.dailyTotalCallingDrivers.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartDataDriversCalling = spotsCalling;
|
||||
final List<FlSpot> spotsTotalMatchingNotes = driversData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.dailyMatchingNotes.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartDataDriversMatchingNotes = spotsTotalMatchingNotes;
|
||||
if (isCompare) {
|
||||
chartDataEmployeerama1Compare = [];
|
||||
chartDataEmployeeshahdCompare = [];
|
||||
chartDataEmployeeRama2Compare = [];
|
||||
chartDataEmployeeSefer4Compare = [];
|
||||
} else {
|
||||
chartDataEmployeerama1 = [];
|
||||
chartDataEmployeeshahd = [];
|
||||
chartDataEmployeeRama2 = [];
|
||||
chartDataEmployeeSefer4 = [];
|
||||
}
|
||||
|
||||
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 {
|
||||
await fetch();
|
||||
await fetchRides();
|
||||
await fetchDrivers();
|
||||
await fetchEmployee();
|
||||
Future<void> fetchEditorCalls({bool isCompare = false}) async {
|
||||
try {
|
||||
DateTime start = isCompare ? compareStartDate! : startDate!;
|
||||
DateTime end = isCompare ? compareEndDate! : endDate!;
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getEditorStatsCalls, payload: _getPayload(start, end));
|
||||
|
||||
if (isCompare) {
|
||||
chartDataCallsrama1Compare = [];
|
||||
chartDataCallsShahdCompare = [];
|
||||
chartDataCallsRama2Compare = [];
|
||||
chartDataCallsSefer4Compare = [];
|
||||
} else {
|
||||
chartDataCallsrama1 = [];
|
||||
chartDataCallsShahd = [];
|
||||
chartDataCallsRama2 = [];
|
||||
chartDataCallsSefer4 = [];
|
||||
}
|
||||
|
||||
if (res == 'failure') return;
|
||||
|
||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||
if (jsonResponse['status'] == 'failure') return;
|
||||
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
if (jsonData.isEmpty) return;
|
||||
|
||||
Map<String, Map<String, double>> dateNameMap = {};
|
||||
|
||||
for (var item in jsonData) {
|
||||
String dateKeyStr = item['date'] ?? item['day'];
|
||||
String name = item['NAME'].toString().toLowerCase().trim();
|
||||
double count = double.tryParse(item['count'].toString()) ?? 0.0;
|
||||
|
||||
if (!dateNameMap.containsKey(dateKeyStr)) {
|
||||
dateNameMap[dateKeyStr] = {};
|
||||
}
|
||||
|
||||
if (dateNameMap[dateKeyStr]!.containsKey(name)) {
|
||||
dateNameMap[dateKeyStr]![name] =
|
||||
dateNameMap[dateKeyStr]![name]! + count;
|
||||
} else {
|
||||
dateNameMap[dateKeyStr]![name] = count;
|
||||
}
|
||||
}
|
||||
|
||||
final targetLists = isCompare
|
||||
? {
|
||||
'rama1': chartDataCallsrama1Compare,
|
||||
'shahd': chartDataCallsShahdCompare,
|
||||
'rama2': chartDataCallsRama2Compare,
|
||||
'mayar': chartDataCallsSefer4Compare,
|
||||
}
|
||||
: {
|
||||
'rama1': chartDataCallsrama1,
|
||||
'shahd': chartDataCallsShahd,
|
||||
'rama2': chartDataCallsRama2,
|
||||
'mayar': chartDataCallsSefer4,
|
||||
};
|
||||
|
||||
int totalDays = end.difference(start).inDays + 1;
|
||||
|
||||
targetLists.forEach((key, listToFill) {
|
||||
for (int i = 0; i < totalDays; i++) {
|
||||
DateTime currentDate = start.add(Duration(days: i));
|
||||
String currentDateStr = DateFormat('yyyy-MM-dd').format(currentDate);
|
||||
|
||||
double value = 0;
|
||||
Map<String, double>? dayData = dateNameMap[currentDateStr];
|
||||
|
||||
if (dayData != null) {
|
||||
// if (key == 'mayar_group') {
|
||||
// value = (dayData['mayar'] ?? 0) +
|
||||
// (dayData['rama1'] ?? 0) +
|
||||
// (dayData['sefer4'] ?? 0);
|
||||
// } else {
|
||||
value = dayData[key] ?? 0;
|
||||
// }
|
||||
}
|
||||
listToFill.add(FlSpot((i + 1).toDouble(), value));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Log.print('Error in fetchEditorCalls: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// --- 🔴 FIXED: Fetch Employment Stats with Unique Check ---
|
||||
Future<void> fetchEmploymentStats() async {
|
||||
try {
|
||||
// لا نستخدم .clear() هنا، سنقوم باستبدال القائمة بالكامل في النهاية
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getEmployeeDriverAfterCallingRegister,
|
||||
payload: _getPayload(startDate!, endDate!));
|
||||
|
||||
if (res == 'failure') return;
|
||||
|
||||
var jsonResponse = jsonDecode(res);
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
if (jsonResponse['message'] != null &&
|
||||
jsonResponse['message']['data'] != null) {
|
||||
List<dynamic> data = jsonResponse['message']['data'];
|
||||
|
||||
List<String> allowedNames = ['shahd', 'mayar', 'rama1', 'rama2'];
|
||||
|
||||
// استخدام Map لضمان عدم تكرار الأسماء (تجميع القيم)
|
||||
Map<String, int> uniqueMap = {};
|
||||
|
||||
for (var item in data) {
|
||||
String name =
|
||||
item['employmentType'].toString().toLowerCase().trim();
|
||||
int count = int.tryParse(item['count'].toString()) ?? 0;
|
||||
|
||||
if (allowedNames.contains(name)) {
|
||||
if (uniqueMap.containsKey(name)) {
|
||||
uniqueMap[name] = uniqueMap[name]! + count;
|
||||
} else {
|
||||
uniqueMap[name] = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// تحويل الـ Map إلى القائمة النهائية
|
||||
List<Map<String, dynamic>> tempList = [];
|
||||
uniqueMap.forEach((key, value) {
|
||||
tempList.add({'name': key, 'count': value});
|
||||
});
|
||||
|
||||
// استبدال القائمة القديمة بالقائمة الجديدة النظيفة
|
||||
employmentStatsList = tempList;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error fetchEmploymentStats: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fetch Daily Notes Log ---
|
||||
Future<void> fetchDailyNotes(DateTime date) async {
|
||||
try {
|
||||
isLoadingNotes = true;
|
||||
dailyNotesList.clear();
|
||||
update();
|
||||
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.getNotesForEmployee,
|
||||
payload: {"date": DateFormat('yyyy-MM-dd').format(date)});
|
||||
|
||||
if (res != 'failure') {
|
||||
var jsonResponse = (res);
|
||||
if (jsonResponse['status'] == 'success') {
|
||||
dailyNotesList = jsonResponse['message'];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error fetchDailyNotes: $e");
|
||||
} finally {
|
||||
isLoadingNotes = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,15 @@ class OtpHelper extends GetxController {
|
||||
/// إرسال OTP
|
||||
static Future<bool> sendOtp(String phoneNumber) async {
|
||||
try {
|
||||
// await CRUD().getJWT();
|
||||
final response = await CRUD().post(
|
||||
link: _sendOtpUrl,
|
||||
payload: {'receiver': phoneNumber},
|
||||
);
|
||||
|
||||
// Log.print('_sendOtpUrl: ${_sendOtpUrl}');
|
||||
// Log.print('response: ${response}');
|
||||
if (response != 'failure') {
|
||||
mySnackeBarError('تم إرسال رمز التحقق إلى رقمك عبر WhatsApp');
|
||||
mySnackbarSuccess('تم إرسال رمز التحقق إلى رقمك عبر WhatsApp');
|
||||
return true;
|
||||
} else {
|
||||
mySnackeBarError('حدث خطأ من الخادم. حاول مجددًا.');
|
||||
|
||||
37
lib/controller/drivers/driver_not_active_controller.dart
Normal file
37
lib/controller/drivers/driver_not_active_controller.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/links.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class DriverController extends GetxController {
|
||||
List drivers = [];
|
||||
Map driverDetails = {};
|
||||
|
||||
// جلب السائقين pending
|
||||
getDriversPending() async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.getDriversPending, // رابط drivers_pending_list.php
|
||||
payload: {},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
drivers = (res)['message'];
|
||||
update(['drivers']); // تحديث الـ UI
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to load drivers');
|
||||
}
|
||||
}
|
||||
|
||||
// جلب تفاصيل سائق واحد
|
||||
getDriverDetails(String driverId) async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.getDriverDetails, // رابط driver_details.php
|
||||
payload: {"id": driverId},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
driverDetails = (res)['message'];
|
||||
update(['driverDetails']); // تحديث صفحة التفاصيل
|
||||
} else {
|
||||
Get.snackbar('Error', 'Failed to load driver details');
|
||||
}
|
||||
}
|
||||
}
|
||||
59
lib/controller/firebase/notification_service.dart
Normal file
59
lib/controller/firebase/notification_service.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../print.dart';
|
||||
|
||||
class NotificationService {
|
||||
// تأكد من أن هذا هو الرابط الصحيح لملف الإرسال
|
||||
static const String _serverUrl =
|
||||
'https://syria.intaleq.xyz/intaleq/fcm/send_fcm.php';
|
||||
|
||||
static Future<void> sendNotification({
|
||||
required String target,
|
||||
required String title,
|
||||
required String body,
|
||||
required String? category, // <-- [الإضافة الأولى]
|
||||
String? tone,
|
||||
List<String>? driverList,
|
||||
bool isTopic = false,
|
||||
}) async {
|
||||
try {
|
||||
final Map<String, dynamic> payload = {
|
||||
'target': target,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'isTopic': isTopic,
|
||||
};
|
||||
|
||||
if (category != null) {
|
||||
payload['category'] = category; // <-- [الإضافة الثانية]
|
||||
}
|
||||
|
||||
if (tone != null) {
|
||||
payload['tone'] = tone;
|
||||
}
|
||||
|
||||
if (driverList != null) {
|
||||
// [مهم] تطبيق السائق يرسل passengerList
|
||||
payload['passengerList'] = jsonEncode(driverList);
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(_serverUrl),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: jsonEncode(payload),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('✅ Notification sent successfully.');
|
||||
} else {
|
||||
print(
|
||||
'❌ Failed to send notification. Status code: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ An error occurred while sending notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
@@ -16,9 +14,7 @@ import '../../constant/links.dart';
|
||||
import '../../env/env.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/widgets/elevated_btn.dart';
|
||||
import 'device_info.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
import 'security_checks.dart';
|
||||
|
||||
class CRUD {
|
||||
@@ -225,7 +221,7 @@ class CRUD {
|
||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
print('fingerPrint: ${fingerPrint}');
|
||||
|
||||
await SecurityChecks.isDeviceRootedFromNative(Get.context!);
|
||||
//await SecurityChecks.isDeviceRootedFromNative(Get.context!);
|
||||
|
||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||
var payload = {
|
||||
@@ -239,11 +235,11 @@ class CRUD {
|
||||
Uri.parse(AppLink.loginWalletAdmin),
|
||||
body: payload,
|
||||
);
|
||||
// Log.print('response.request: ${response1.request}');
|
||||
// Log.print('response.body: ${response1.body}');
|
||||
// print(payload);
|
||||
// Log.print(
|
||||
// 'jsonDecode(response1.body)["jwt"]: ${jsonDecode(response1.body)['jwt']}');
|
||||
Log.print('response.request: ${response1.request}');
|
||||
Log.print('response.body: ${response1.body}');
|
||||
print(payload);
|
||||
Log.print(
|
||||
'jsonDecode(response1.body)["jwt"]: ${jsonDecode(response1.body)['jwt']}');
|
||||
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
|
||||
return jsonDecode(response1.body)['jwt'].toString();
|
||||
}
|
||||
@@ -268,9 +264,9 @@ class CRUD {
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
},
|
||||
);
|
||||
Log.print('response.request:${response.request}');
|
||||
Log.print('response.body: ${response.body}');
|
||||
Log.print('payload:$payload');
|
||||
// Log.print('response.request:${response.request}');
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// Log.print('payload:$payload');
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
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 {
|
||||
static Future<String> getDeviceFingerprint() async {
|
||||
@@ -27,13 +21,17 @@ class DeviceHelper {
|
||||
// Fetch iOS-specific device information
|
||||
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
||||
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 {
|
||||
throw UnsupportedError('Unsupported platform');
|
||||
}
|
||||
|
||||
// Extract relevant device information
|
||||
final String deviceId = Platform.isAndroid
|
||||
? deviceData['androidId'] ?? deviceData['serialNumber'] ?? 'unknown'
|
||||
? deviceData['fingerprint'] ?? 'unknown'
|
||||
: deviceData['identifierForVendor'] ?? 'unknown';
|
||||
|
||||
final String deviceModel = deviceData['model'] ?? 'unknown';
|
||||
@@ -45,6 +43,7 @@ class DeviceHelper {
|
||||
|
||||
// Generate and return the encrypted fingerprint
|
||||
final String fingerprint = '${deviceId}_${deviceModel}_$osVersion';
|
||||
|
||||
// print(EncryptionHelper.instance.encryptData(fingerprint));
|
||||
return (fingerprint);
|
||||
} catch (e) {
|
||||
|
||||
@@ -30,10 +30,10 @@ class EncryptionHelper {
|
||||
}
|
||||
debugPrint("Initializing EncryptionHelper...");
|
||||
var keyOfApp = r(Env.keyOfApp).toString().split(Env.addd)[0];
|
||||
Log.print('keyOfApp: ${keyOfApp}');
|
||||
// Log.print('keyOfApp: ${keyOfApp}');
|
||||
var initializationVector =
|
||||
r(Env.initializationVector).toString().split(Env.addd)[0];
|
||||
Log.print('initializationVector: ${initializationVector}');
|
||||
// Log.print('initializationVector: ${initializationVector}');
|
||||
|
||||
// Set the global instance
|
||||
_instance = EncryptionHelper._(
|
||||
|
||||
@@ -13,8 +13,10 @@ import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
|
||||
class ImageController extends GetxController {
|
||||
File? myImage;
|
||||
@@ -152,7 +154,7 @@ class ImageController extends GetxController {
|
||||
} catch (e) {
|
||||
print('Error in choosImage: $e');
|
||||
Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
backgroundColor: AppColor.primaryColor);
|
||||
backgroundColor: AppColor.redColor);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
@@ -241,20 +243,25 @@ class ImageController extends GetxController {
|
||||
'POST',
|
||||
Uri.parse(link),
|
||||
);
|
||||
|
||||
Log.print('request: ${request}');
|
||||
var length = await file.length();
|
||||
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(
|
||||
'image',
|
||||
stream,
|
||||
length,
|
||||
filename: basename(file.path),
|
||||
);
|
||||
request.headers.addAll({
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials.toString()))}',
|
||||
});
|
||||
request.headers.addAll(headers);
|
||||
// Set the file name to the driverID
|
||||
|
||||
request.files.add(
|
||||
http.MultipartFile(
|
||||
'image',
|
||||
@@ -270,8 +277,10 @@ class ImageController extends GetxController {
|
||||
var res = await http.Response.fromStream(myrequest);
|
||||
if (res.statusCode == 200) {
|
||||
Log.print('jsonDecode(res.body): ${jsonDecode(res.body)}');
|
||||
|
||||
Get.snackbar('title', 'message', backgroundColor: AppColor.greenColor);
|
||||
if (jsonDecode(res.body)['status'] == 'Image uploaded successfully!') {
|
||||
Get.snackbar('Success'.tr, 'Image uploaded successfully!'.tr,
|
||||
backgroundColor: AppColor.greenColor);
|
||||
}
|
||||
return jsonDecode(res.body);
|
||||
} else {
|
||||
throw Exception(
|
||||
|
||||
@@ -43,6 +43,38 @@ class WalletController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future addDriverWallet(String paymentMethod, driverID, point, phone) async {
|
||||
// paymentToken = await generateToken(count);
|
||||
// var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addFromAdmin, payload: {
|
||||
'driverID': driverID.toString(),
|
||||
'paymentID': 'gift_connect_$driverID${DateTime.timestamp()}'.toString(),
|
||||
'amount': point,
|
||||
'token': 'gift_connect',
|
||||
'paymentMethod': paymentMethod,
|
||||
'phone': phone,
|
||||
});
|
||||
}
|
||||
|
||||
Future addDrivergift3000(String paymentMethod, driverID, point, phone) async {
|
||||
// paymentToken = await generateToken(count);
|
||||
// var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
var res = await CRUD().postWallet(link: AppLink.add300ToDriver, payload: {
|
||||
'driverID': driverID.toString(),
|
||||
'paymentID': paymentMethod,
|
||||
'amount': point,
|
||||
'token': 'gift_connect_30000',
|
||||
'paymentMethod': paymentMethod,
|
||||
'phone': phone,
|
||||
});
|
||||
if (res != 'failure') {
|
||||
Get.snackbar('success', 'addDrivergift3000',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
} else {
|
||||
Get.snackbar('error', res, backgroundColor: AppColor.redColor);
|
||||
}
|
||||
}
|
||||
|
||||
Future addSeferWallet(String point, driverID) async {
|
||||
var amount = (int.parse(point) * -1).toStringAsFixed(0);
|
||||
var seferToken = await generateTokenDriver(amount, driverID);
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:sefer_admin1/views/widgets/my_textField.dart';
|
||||
|
||||
import '../constant/style.dart';
|
||||
import '../print.dart';
|
||||
import 'firebase/notification_service.dart';
|
||||
|
||||
class NotificationController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
@@ -18,14 +19,14 @@ class NotificationController extends GetxController {
|
||||
List<String> tokensDriver = [];
|
||||
List<String> tokensPassengers = [];
|
||||
|
||||
getTokensDrivers() async {
|
||||
await FirebaseMessagesController().loadAllPagesAndSendNotifications();
|
||||
}
|
||||
// getTokensDrivers() async {
|
||||
// await FirebaseMessagesController().loadAllPagesAndSendNotifications();
|
||||
// }
|
||||
|
||||
getTokensPassengers() async {
|
||||
await FirebaseMessagesController()
|
||||
.loadAllPagesAndSendNotificationsPassengers();
|
||||
}
|
||||
// getTokensPassengers() async {
|
||||
// await FirebaseMessagesController()
|
||||
// .loadAllPagesAndSendNotificationsPassengers();
|
||||
// }
|
||||
|
||||
Future<dynamic> sendNotificationDrivers() {
|
||||
return Get.defaultDialog(
|
||||
@@ -60,34 +61,40 @@ class NotificationController extends GetxController {
|
||||
// tokensDriver = box.read(BoxName.tokensDrivers)['message'];
|
||||
// Log.print('tokensDriver: ${tokensDriver}');
|
||||
// if (formKey.currentState!.validate()) {
|
||||
box.read(BoxName.tokensDrivers)['message'].length;
|
||||
for (var i = 0;
|
||||
i < box.read(BoxName.tokensDrivers)['message'].length;
|
||||
i++) {
|
||||
// for (var i = 0; i < 2; i++) {
|
||||
// print(i);
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.addNotificationCaptain, payload: {
|
||||
"driverID": box
|
||||
.read(BoxName.tokensDrivers)['message'][i]['id']
|
||||
.toString(),
|
||||
"title": title.text,
|
||||
"body": body.text,
|
||||
"isPin": 'unPin',
|
||||
});
|
||||
Log.print(
|
||||
'res: ${res}for ${box.read(BoxName.tokensDrivers)['message'][i]['id']}');
|
||||
// Log.print('tokensDriver[i]: ${tokensDriver[i]}');
|
||||
Future.delayed(const Duration(microseconds: 50));
|
||||
|
||||
FirebaseMessagesController().sendNotificationToAnyWithoutData(
|
||||
title.text,
|
||||
body.text,
|
||||
box
|
||||
.read(BoxName.tokensDrivers)['message'][i]['token']
|
||||
.toString(),
|
||||
'tone2.wav');
|
||||
}
|
||||
// box.read(BoxName.tokensDrivers)['message'].length;
|
||||
// for (var i = 0;
|
||||
// i < box.read(BoxName.tokensDrivers)['message'].length;
|
||||
// i++) {
|
||||
// // for (var i = 0; i < 2; i++) {
|
||||
// // print(i);
|
||||
// var res = await CRUD()
|
||||
// .post(link: AppLink.addNotificationCaptain, payload: {
|
||||
// "driverID": box
|
||||
// .read(BoxName.tokensDrivers)['message'][i]['id']
|
||||
// .toString(),
|
||||
// "title": title.text,
|
||||
// "body": body.text,
|
||||
// "isPin": 'unPin',
|
||||
// });
|
||||
// Log.print(
|
||||
// 'res: ${res}for ${box.read(BoxName.tokensDrivers)['message'][i]['id']}');
|
||||
// // Log.print('tokensDriver[i]: ${tokensDriver[i]}');
|
||||
// Future.delayed(const Duration(microseconds: 50));
|
||||
NotificationService.sendNotification(
|
||||
target: 'drivers', // الإرسال لجميع المشتركين في "service"
|
||||
title: title.text,
|
||||
body: body.text,
|
||||
isTopic: true,
|
||||
category: 'fromAdmin', // فئة توضح نوع الإشعار
|
||||
);
|
||||
// FirebaseMessagesController().sendNotificationToAnyWithoutData(
|
||||
// title.text,
|
||||
// body.text,
|
||||
// box
|
||||
// .read(BoxName.tokensDrivers)['message'][i]['token']
|
||||
// .toString(),
|
||||
// 'tone2.wav');
|
||||
// }
|
||||
Get.back();
|
||||
// }
|
||||
}),
|
||||
@@ -129,38 +136,45 @@ class NotificationController extends GetxController {
|
||||
title: 'send'.tr,
|
||||
onPressed: () async {
|
||||
// tokensPassengers = box.read(BoxName.tokensPassengers);
|
||||
var tokensPassengersData =
|
||||
box.read(BoxName.tokensPassengers)['data'];
|
||||
// var tokensPassengersData =
|
||||
// box.read(BoxName.tokensPassengers)['data'];
|
||||
|
||||
// Debug print to check structure of the 'data' field
|
||||
print('Tokens Passengers Data: $tokensPassengersData');
|
||||
// // Debug print to check structure of the 'data' field
|
||||
// print('Tokens Passengers Data: $tokensPassengersData');
|
||||
|
||||
if (tokensPassengersData is List) {
|
||||
for (var i = 0; i < tokensPassengersData.length; i++) {
|
||||
if (formKey.currentState!.validate()) {
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.addNotificationPassenger, payload: {
|
||||
"passenger_id":
|
||||
tokensPassengersData[i]['passengerID'].toString(),
|
||||
"title": title.text,
|
||||
"body": body.text,
|
||||
});
|
||||
Log.print('res: ${res}');
|
||||
FirebaseMessagesController()
|
||||
.sendNotificationToAnyWithoutData(
|
||||
title.text,
|
||||
body.text,
|
||||
tokensPassengersData[i]['token']
|
||||
.toString(), // Access token correctly
|
||||
'order.wav',
|
||||
);
|
||||
}
|
||||
}
|
||||
Get.back();
|
||||
} else {
|
||||
// Handle the case where 'data' is not a list
|
||||
print('Data is not a list: $tokensPassengersData');
|
||||
}
|
||||
// if (tokensPassengersData is List) {
|
||||
// for (var i = 0; i < tokensPassengersData.length; i++) {
|
||||
// if (formKey.currentState!.validate()) {
|
||||
// var res = await CRUD()
|
||||
// .post(link: AppLink.addNotificationPassenger, payload: {
|
||||
// "passenger_id":
|
||||
// tokensPassengersData[i]['passengerID'].toString(),
|
||||
// "title": title.text,
|
||||
// "body": body.text,
|
||||
// });
|
||||
// Log.print('res: ${res}');
|
||||
// FirebaseMessagesController()
|
||||
// .sendNotificationToAnyWithoutData(
|
||||
// title.text,
|
||||
// body.text,
|
||||
// tokensPassengersData[i]['token']
|
||||
// .toString(), // Access token correctly
|
||||
// 'order.wav',
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
NotificationService.sendNotification(
|
||||
target: 'passengers', // الإرسال لجميع المشتركين في "service"
|
||||
title: title.text,
|
||||
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(
|
||||
title: 'cancel',
|
||||
|
||||
139
lib/controller/rides/ride_lookup_controller.dart
Normal file
139
lib/controller/rides/ride_lookup_controller.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/links.dart';
|
||||
import '../functions/crud.dart';
|
||||
|
||||
class RideLookupController extends GetxController {
|
||||
final TextEditingController phoneCtrl = TextEditingController();
|
||||
|
||||
bool isLoading = false;
|
||||
Map<String, dynamic>? passenger; // {id, first_name, last_name, phone}
|
||||
Map<String, dynamic>? ride; // Ride details
|
||||
|
||||
// Status filter for the search tab
|
||||
String currentStatusFilter = '';
|
||||
|
||||
// Whitelist of allowed statuses for the Update Dropdown
|
||||
// UPDATED: Matches the exact types you requested
|
||||
final List<String> statusOptions = const [
|
||||
'Pending',
|
||||
'Accepted',
|
||||
'EnRoute',
|
||||
'Arrived',
|
||||
'Started',
|
||||
'Completed',
|
||||
'Canceled',
|
||||
];
|
||||
|
||||
String? selectedStatus;
|
||||
|
||||
// Hydrate dropdown value from the current ride data
|
||||
void hydrateSelectedFromRide() {
|
||||
final cur = (ride?['status'] ?? '') as String;
|
||||
selectedStatus = statusOptions.contains(cur) ? cur : null;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<bool> updateRideStatus({String? note}) async {
|
||||
if (ride == null) return false;
|
||||
if (selectedStatus == null || selectedStatus!.isEmpty) return false;
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
final res = await CRUD().post(
|
||||
link: AppLink.admin_update_ride_status,
|
||||
payload: {
|
||||
'id': "${ride!['id']}",
|
||||
'status': selectedStatus!,
|
||||
if (note != null && note.trim().isNotEmpty) 'reason': note.trim(),
|
||||
},
|
||||
);
|
||||
|
||||
final d = jsonDecode(res);
|
||||
final ok = (d['status'] == 'success');
|
||||
|
||||
if (ok) {
|
||||
// Update local ride details from response
|
||||
final updated = (d['message'] ?? d)['ride'];
|
||||
if (updated != null) {
|
||||
ride = Map<String, dynamic>.from(updated);
|
||||
}
|
||||
update();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (_) {
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// Updated to accept status filter
|
||||
Future<bool> searchLatest({String? status}) async {
|
||||
final phone = phoneCtrl.text.trim();
|
||||
|
||||
// If status is passed, update the current filter
|
||||
if (status != null) {
|
||||
currentStatusFilter = status;
|
||||
}
|
||||
|
||||
// If phone is empty, we stop unless your API supports fetching "latest of all users"
|
||||
if (phone.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
final res = await CRUD().post(
|
||||
link: AppLink.admin_get_rides_by_phone,
|
||||
payload: {
|
||||
'phone': phone,
|
||||
// If filter is 'All', send empty string to PHP, otherwise send the exact status
|
||||
'status': currentStatusFilter == 'All' ? '' : currentStatusFilter,
|
||||
},
|
||||
);
|
||||
|
||||
final d = res;
|
||||
|
||||
if (d['status'] == 'success') {
|
||||
passenger = (d['message'] ?? d)['passenger'];
|
||||
ride = (d['message'] ?? d)['ride'];
|
||||
|
||||
// Hydrate the dropdown for the update section based on the fetched ride
|
||||
hydrateSelectedFromRide();
|
||||
|
||||
update();
|
||||
return true;
|
||||
} else {
|
||||
passenger = null;
|
||||
ride = null;
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
} catch (_) {
|
||||
passenger = null;
|
||||
ride = null;
|
||||
update();
|
||||
return false;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
String rideHeader() {
|
||||
if (ride == null) return '';
|
||||
final id = ride!['id'] ?? '';
|
||||
final st = (ride!['status'] ?? '—').toString();
|
||||
return "Ride #$id — $st";
|
||||
}
|
||||
}
|
||||
25358
lib/env/env.g.dart
vendored
25358
lib/env/env.g.dart
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.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_storage/get_storage.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/functions/encrypt_decrypt.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'models/db_sql.dart';
|
||||
import 'views/admin/admin_home_page.dart';
|
||||
|
||||
final box = GetStorage();
|
||||
const storage = FlutterSecureStorage();
|
||||
@@ -31,12 +29,13 @@ DbSql sql = DbSql.instance;
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await GetStorage.init();
|
||||
await initializeDateFormatting('ar', null);
|
||||
await EncryptionHelper.initialize();
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
await FirebaseMessagesController().requestFirebaseMessagingPermission();
|
||||
// await FirebaseMessagesController().requestFirebaseMessagingPermission();
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
||||
|
||||
@@ -51,7 +50,7 @@ void main() async {
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
} // Enable Crashlytics collection
|
||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
|
||||
// FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
|
||||
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
@@ -1,180 +1,237 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_admin1/constant/colors.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/notification_controller.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';
|
||||
|
||||
// Please make sure all these imports are correct for your project structure
|
||||
// تأكد من صحة المسارات
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../invoice/invoice_list_page.dart';
|
||||
import '../widgets/my_scafold.dart';
|
||||
import '../widgets/my_textField.dart';
|
||||
import '../invoice/add_invoice_page.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/monitor_ride.dart';
|
||||
import 'employee/employee_page.dart';
|
||||
import 'packages.dart';
|
||||
import 'passenger/passenger.dart';
|
||||
import 'rides/rides.dart';
|
||||
import 'rides/ride_lookup_page.dart';
|
||||
import 'static/static.dart';
|
||||
import 'wallet/wallet.dart';
|
||||
|
||||
class AdminHomePage extends StatelessWidget {
|
||||
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();
|
||||
|
||||
// حساب عدد الأعمدة
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
// Make sure DashboardController is initialized
|
||||
final DashboardController 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 = [
|
||||
// --- عناصر يراها الجميع ---
|
||||
{
|
||||
'title': 'الركاب',
|
||||
'icon': Icons.people_alt_outlined,
|
||||
'onPressed': () => Get.to(() => Passengrs(),
|
||||
transition: Transition.rightToLeftWithFade)
|
||||
'icon': Icons.people_alt_rounded,
|
||||
'color': Colors.blueAccent,
|
||||
'onPressed': () =>
|
||||
Get.to(() => Passengrs(), transition: Transition.fadeIn)
|
||||
},
|
||||
{
|
||||
'title': 'الكباتن',
|
||||
'icon': Icons.sports_motorsports_outlined,
|
||||
'icon': Icons.sports_motorsports_rounded,
|
||||
'color': Colors.orangeAccent,
|
||||
'onPressed': () =>
|
||||
Get.to(() => Captain(), transition: Transition.rightToLeftWithFade)
|
||||
},
|
||||
{
|
||||
'title': 'المحفظة',
|
||||
'icon': Icons.account_balance_wallet_outlined,
|
||||
'onPressed': () =>
|
||||
Get.to(() => Wallet(), transition: Transition.rightToLeftWithFade)
|
||||
Get.to(() => CaptainsPage(), transition: Transition.fadeIn)
|
||||
},
|
||||
{
|
||||
'title': 'الرحلات',
|
||||
'icon': Icons.directions_car_filled_outlined,
|
||||
'icon': Icons.directions_car_filled_rounded,
|
||||
'color': Colors.indigoAccent,
|
||||
'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': 'الإحصائيات',
|
||||
'icon': Icons.bar_chart_outlined,
|
||||
'icon': Icons.bar_chart_rounded,
|
||||
'color': Colors.teal,
|
||||
'onPressed': () async {
|
||||
await Get.put(StaticController()).getAll();
|
||||
Get.to(() => const StaticDash());
|
||||
}
|
||||
},
|
||||
{
|
||||
'title': 'إرسال واتساب للسائقين',
|
||||
'icon': Icons.message_outlined,
|
||||
'iconColor': Colors.green.shade600,
|
||||
'onPressed': () => _showWhatsAppDialog(context)
|
||||
},
|
||||
{
|
||||
'title': 'إرسال إشعار للسائقين',
|
||||
'icon': Icons.notifications_active_outlined,
|
||||
'onPressed': () async =>
|
||||
await Get.put(NotificationController()).getTokensDrivers()
|
||||
},
|
||||
{
|
||||
'title': 'إرسال إشعار للركاب',
|
||||
'icon': Icons.notification_important_outlined,
|
||||
'onPressed': () async =>
|
||||
await Get.put(NotificationController()).getTokensPassengers()
|
||||
},
|
||||
{
|
||||
'title': 'تسجيل كابتن جديد',
|
||||
'icon': Icons.person_add_alt_1_outlined,
|
||||
'onPressed': () async {
|
||||
await Get.put(RegisterCaptainController())
|
||||
.getDriverNotCompleteRegistration();
|
||||
Get.to(() => const DriversCantRegister());
|
||||
}
|
||||
},
|
||||
{
|
||||
'title': 'تحديث الباقات',
|
||||
'icon': Icons.inventory_2_outlined,
|
||||
'onPressed': () => Get.to(() => PackageUpdateScreen())
|
||||
},
|
||||
{
|
||||
'title': 'الموظفون',
|
||||
'icon': Icons.badge_outlined,
|
||||
'onPressed': () => Get.to(() => EmployeePage())
|
||||
},
|
||||
{
|
||||
'title': 'أفضل السائقين',
|
||||
'icon': Icons.star_border_purple500_outlined,
|
||||
'onPressed': () => Get.to(() => DriverTheBest())
|
||||
},
|
||||
{
|
||||
'title': 'إضافة فاتورة',
|
||||
'icon': Icons.post_add_outlined,
|
||||
// 'onPressed': () => Get.to(() => AddInvoicePage())
|
||||
'onPressed': () => Get.to(() => InvoiceListPage())
|
||||
},
|
||||
{
|
||||
'title': 'إضافة جهاز كمسؤول',
|
||||
'icon': Icons.admin_panel_settings_outlined,
|
||||
'onPressed': () async => await CRUD()
|
||||
.post(link: AppLink.addAdminUser, payload: {'name': 'b'})
|
||||
},
|
||||
|
||||
// هذا هو الجزء الذي طلبته تحديداً
|
||||
if (isSuperAdmin)
|
||||
{
|
||||
'title': 'واتساب سائقين',
|
||||
'icon': Icons.message_rounded,
|
||||
'color': Colors.green,
|
||||
'onPressed': () => _showWhatsAppDialog(context)
|
||||
},
|
||||
|
||||
// --- عودة للعناصر العامة (أو يمكنك تقييدها أيضاً) ---
|
||||
|
||||
if (isSuperAdmin)
|
||||
{
|
||||
'title': 'إشعار للسائقين',
|
||||
'icon': Icons.notifications_active_rounded,
|
||||
'color': Colors.deepOrange,
|
||||
'onPressed': () async =>
|
||||
await Get.put(NotificationController()).sendNotificationDrivers()
|
||||
},
|
||||
if (isSuperAdmin)
|
||||
{
|
||||
'title': 'إشعار للركاب',
|
||||
'icon': Icons.notification_important_rounded,
|
||||
'color': Colors.pinkAccent,
|
||||
'onPressed': () async => await Get.put(NotificationController())
|
||||
.sendNotificationPassengers()
|
||||
},
|
||||
if (isSuperAdmin)
|
||||
{
|
||||
'title': 'تسجيل كابتن',
|
||||
'icon': Icons.person_add_alt_1_rounded,
|
||||
'color': Colors.cyan,
|
||||
'onPressed': () => Get.to(() => DriversPendingPage())
|
||||
},
|
||||
|
||||
if (isSuperAdmin) // تحديث الباقات للمدير فقط
|
||||
{
|
||||
'title': 'تحديث الباقات',
|
||||
'icon': Icons.inventory_2_rounded,
|
||||
'color': Colors.brown,
|
||||
'onPressed': () => Get.to(() => PackageUpdateScreen())
|
||||
},
|
||||
|
||||
if (isSuperAdmin) // الموظفون للمدير فقط
|
||||
{
|
||||
'title': 'الموظفون',
|
||||
'icon': Icons.badge_rounded,
|
||||
'color': Colors.blueGrey,
|
||||
'onPressed': () => Get.to(() => EmployeePage())
|
||||
},
|
||||
|
||||
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(
|
||||
title: 'لوحة التحكم الرئيسية',
|
||||
action: IconButton(
|
||||
onPressed: () async {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await dashboardController.getDashBoard();
|
||||
},
|
||||
icon: const Icon(Icons.refresh, color: AppColor.primaryColor, size: 28),
|
||||
tooltip: 'تحديث',
|
||||
),
|
||||
body: [
|
||||
GetBuilder<DashboardController>(builder: (controller) {
|
||||
child: GetBuilder<DashboardController>(builder: (controller) {
|
||||
if (controller.dashbord.isEmpty) {
|
||||
return const MyCircularProgressIndicator();
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Main data map for easier access
|
||||
final data = controller.dashbord[0];
|
||||
|
||||
// Stat cards list with Arabic titles
|
||||
// إحصائيات لوحة التحكم
|
||||
final List<Map<String, dynamic>> statCards = [
|
||||
{
|
||||
'title': 'رصيد الرسائل',
|
||||
'value': controller.creditSMS.toString(),
|
||||
'icon': Icons.sms_outlined,
|
||||
'color': Colors.lightBlue
|
||||
},
|
||||
// يمكنك تطبيق نفس المنطق هنا لإخفاء الأرباح عن الموظفين العاديين
|
||||
if (isSuperAdmin)
|
||||
{
|
||||
'title': 'رصيد الرسائل',
|
||||
'value': controller.creditSMS.toString(),
|
||||
'icon': Icons.sms_outlined,
|
||||
'color': Colors.lightBlue
|
||||
},
|
||||
{
|
||||
'title': 'الركاب',
|
||||
'value': data['countPassengers'].toString(),
|
||||
@@ -193,12 +250,15 @@ class AdminHomePage extends StatelessWidget {
|
||||
'icon': Icons.calendar_month_outlined,
|
||||
'color': Colors.purple
|
||||
},
|
||||
{
|
||||
'title': 'متوسط التكلفة',
|
||||
'value': _formatCurrency(data['avg_passenger_price']),
|
||||
'icon': Icons.monetization_on_outlined,
|
||||
'color': Colors.green
|
||||
},
|
||||
|
||||
if (isSuperAdmin) // إخفاء الأمور المالية
|
||||
{
|
||||
'title': 'متوسط التكلفة',
|
||||
'value': _formatCurrency(data['avg_passenger_price']),
|
||||
'icon': Icons.monetization_on_outlined,
|
||||
'color': Colors.green
|
||||
},
|
||||
|
||||
{
|
||||
'title': 'الرحلات المكتملة',
|
||||
'value': data['completed_rides'].toString(),
|
||||
@@ -211,24 +271,29 @@ class AdminHomePage extends StatelessWidget {
|
||||
'icon': Icons.cancel_outlined,
|
||||
'color': AppColor.redColor
|
||||
},
|
||||
|
||||
// if (isSuperAdmin) // إخفاء المدفوعات
|
||||
{
|
||||
'title': 'مدفوعات السائقين',
|
||||
'value': _formatCurrency(data['payments']),
|
||||
'icon': Icons.payments_outlined,
|
||||
'color': Colors.indigo
|
||||
},
|
||||
// if (isSuperAdmin)
|
||||
{
|
||||
'title': 'محفظة انطلق',
|
||||
'value': _formatCurrency(data['seferWallet']),
|
||||
'icon': Icons.account_balance_wallet_outlined,
|
||||
'color': Colors.deepOrange
|
||||
},
|
||||
|
||||
{
|
||||
'title': 'عدد التحويلات',
|
||||
'value': data['transfer_from_count'].toString(),
|
||||
'icon': Icons.swap_horiz_outlined,
|
||||
'color': Colors.brown
|
||||
},
|
||||
// ... بقية العناصر التي لا تحتاج إخفاء
|
||||
{
|
||||
'title': 'رحلات الصباح',
|
||||
'value': data['morning_ride_count'].toString(),
|
||||
@@ -248,43 +313,96 @@ class AdminHomePage extends StatelessWidget {
|
||||
'color': Colors.black87
|
||||
},
|
||||
{
|
||||
'title': 'نوع كومفورت',
|
||||
'title': 'كومفورت',
|
||||
'value': data['comfort'].toString(),
|
||||
'icon': Icons.event_seat_outlined,
|
||||
'color': Colors.cyan
|
||||
},
|
||||
{
|
||||
'title': 'نوع سبيد',
|
||||
'title': 'سبيد',
|
||||
'value': data['speed'].toString(),
|
||||
'icon': Icons.speed_outlined,
|
||||
'color': Colors.red.shade700
|
||||
},
|
||||
{
|
||||
'title': 'نوع ليدي',
|
||||
'title': 'ليدي',
|
||||
'value': data['lady'].toString(),
|
||||
'icon': Icons.woman_2_outlined,
|
||||
'color': Colors.pink
|
||||
},
|
||||
];
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
children: [
|
||||
// --- Statistics Grid Section ---
|
||||
AnimationLimiter(
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: _calculateCrossAxisCount(context),
|
||||
mainAxisSpacing: 12.0,
|
||||
crossAxisSpacing: 12.0,
|
||||
childAspectRatio: 1.8,
|
||||
return CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 120.0,
|
||||
floating: true,
|
||||
pinned: true,
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
elevation: 0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
titlePadding:
|
||||
const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||
title: const Text(
|
||||
'لوحة التحكم',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColor.primaryColor,
|
||||
AppColor.primaryColor.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
itemCount: statCards.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
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];
|
||||
return AnimationConfiguration.staggeredGrid(
|
||||
position: index,
|
||||
@@ -292,184 +410,274 @@ class AdminHomePage extends StatelessWidget {
|
||||
columnCount: _calculateCrossAxisCount(context),
|
||||
child: ScaleAnimation(
|
||||
child: FadeInAnimation(
|
||||
child: DashboardStatCard(
|
||||
title: card['title'] as String,
|
||||
value: card['value'].toString(),
|
||||
icon: card['icon'] as IconData,
|
||||
iconColor: card['color'] as Color,
|
||||
valueColor: (card['color'] as Color),
|
||||
child: _buildModernStatCard(
|
||||
title: card['title'],
|
||||
value: card['value'],
|
||||
icon: card['icon'],
|
||||
color: card['color'],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: statCards.length,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
Text("الإجراءات السريعة",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// --- Admin Actions List Section ---
|
||||
AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
itemCount: actionItems.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 10),
|
||||
child: Text(
|
||||
"إدارة النظام",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800]),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding:
|
||||
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];
|
||||
return AnimationConfiguration.staggeredList(
|
||||
return AnimationConfiguration.staggeredGrid(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
columnCount:
|
||||
_calculateCrossAxisCount(context, isSmall: true),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: AdminActionTile(
|
||||
title: item['title'] as String,
|
||||
icon: item['icon'] as IconData,
|
||||
onPressed: item['onPressed'] as void Function(),
|
||||
iconColor: item['iconColor'] as Color?,
|
||||
child: _buildActionCard(
|
||||
context,
|
||||
title: item['title'],
|
||||
icon: item['icon'],
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
isleading: false,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[900],
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionCard(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [color.withOpacity(0.2), color.withOpacity(0.05)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Icon(icon, size: 28, color: color),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showWhatsAppDialog(BuildContext context) {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
title: Text('تأكيد إرسال الرسائل؟'),
|
||||
content: MyTextForm(
|
||||
controller: _messageController,
|
||||
label: 'الرسالة',
|
||||
hint: 'أدخل نص الرسالة هنا',
|
||||
type: TextInputType.text,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: const Text('إرسال رسالة جماعية',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.send, size: 40, color: Colors.green),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
MyTextForm(
|
||||
controller: _messageController,
|
||||
label: 'الرسالة',
|
||||
hint: 'اكتب الرسالة هنا...',
|
||||
type: TextInputType.text,
|
||||
),
|
||||
],
|
||||
),
|
||||
actionsPadding: const EdgeInsets.all(15),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_messageController.clear();
|
||||
Get.back();
|
||||
},
|
||||
child: Text('إلغاء'),
|
||||
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
ElevatedButton(
|
||||
ElevatedButton.icon(
|
||||
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 {
|
||||
if (_messageController.text.isNotEmpty) {
|
||||
Get.back(); // Close dialog first
|
||||
Get.back();
|
||||
var driverPhones =
|
||||
box.read(BoxName.tokensDrivers)['message'] as List?;
|
||||
if (driverPhones == null || driverPhones.isEmpty) {
|
||||
Get.snackbar('خطأ', 'لم يتم العثور على أرقام هواتف للسائقين.',
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
Get.snackbar('تنبيه', 'لا توجد بيانات اتصال للسائقين',
|
||||
backgroundColor: Colors.amber.withOpacity(0.5));
|
||||
return;
|
||||
}
|
||||
|
||||
Get.snackbar('جاري الإرسال', 'بدأت عملية الإرسال في الخلفية...',
|
||||
backgroundColor: Colors.blue.withOpacity(0.3));
|
||||
|
||||
for (var driverData in driverPhones) {
|
||||
if (driverData['phone'] != null) {
|
||||
await CRUD().sendWhatsAppAuth(
|
||||
driverData['phone'].toString(),
|
||||
_messageController.text,
|
||||
);
|
||||
// Random delay to avoid being flagged as spam
|
||||
await Future.delayed(
|
||||
Duration(seconds: Random().nextInt(5) + 2));
|
||||
Duration(seconds: Random().nextInt(3) + 1));
|
||||
}
|
||||
}
|
||||
_messageController.clear();
|
||||
Get.snackbar(
|
||||
'نجاح',
|
||||
'تم إرسال الرسائل بنجاح',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green.shade100,
|
||||
colorText: Colors.black,
|
||||
);
|
||||
Get.snackbar('نجاح', 'تمت العملية بنجاح',
|
||||
backgroundColor: Colors.green.withOpacity(0.5));
|
||||
}
|
||||
},
|
||||
child: Text('إرسال', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Renamed for clarity and improved design
|
||||
class AdminActionTile extends StatelessWidget {
|
||||
const AdminActionTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onPressed,
|
||||
required this.icon,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final VoidCallback onPressed;
|
||||
final IconData icon;
|
||||
final Color? iconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Material(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 18.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2))),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 26,
|
||||
color: iconColor ?? AppColor.primaryColor,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,223 +1,302 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
// تأكد من استيراد مكتبة الاتصال إذا أردت تفعيل زر الاتصال فعلياً
|
||||
// import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/captain_admin_controller.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/captain_admin_controller.dart';
|
||||
import 'captain_details.dart';
|
||||
import 'form_captain.dart';
|
||||
|
||||
class Captain extends StatelessWidget {
|
||||
Captain({super.key});
|
||||
final CaptainAdminController captainAdminController =
|
||||
class CaptainsPage extends StatelessWidget {
|
||||
CaptainsPage({super.key});
|
||||
|
||||
final CaptainAdminController captainController =
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
return MyScafolld(
|
||||
title: '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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
title: 'Search for Captain'.tr,
|
||||
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(
|
||||
height: Get.height * .1,
|
||||
decoration: BoxDecoration(border: Border.all(width: 2)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
title.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
captainAdminController.captainData['message'][0][jsonField]
|
||||
.toString(),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
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],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,167 +1,404 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/captain_admin_controller.dart';
|
||||
import '../../../controller/firebase/firbase_messge.dart';
|
||||
import '../../../main.dart'; // Import main to access myPhone
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import 'form_captain.dart';
|
||||
|
||||
class CaptainsDetailsPage extends StatelessWidget {
|
||||
const CaptainsDetailsPage({super.key});
|
||||
class CaptainDetailsPage extends StatelessWidget {
|
||||
const CaptainDetailsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final arguments = Get.arguments;
|
||||
final Map<String, dynamic> data = arguments['data'];
|
||||
var key = Get.find<CaptainAdminController>().formCaptainPrizeKey;
|
||||
var titleNotify = Get.find<CaptainAdminController>().titleNotify;
|
||||
var bodyNotify = Get.find<CaptainAdminController>().bodyNotify;
|
||||
final Map<String, dynamic> data = Get.arguments['data'];
|
||||
final controller = Get.find<CaptainAdminController>();
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
|
||||
// Define Super Admin Logic
|
||||
final bool isSuperAdmin =
|
||||
myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
return MyScafolld(
|
||||
title: data['first_name'] + ' ' + data['last_name'],
|
||||
title: 'Captain Profile'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Email is ${data['email']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Phone is ${data['phone']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'gender is ${data['gender']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
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(
|
||||
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,
|
||||
),
|
||||
// --- Header Section (Avatar & Name) ---
|
||||
_buildHeaderSection(context, data),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- Personal Information Card ---
|
||||
_buildInfoCard(
|
||||
title: 'Personal Information',
|
||||
icon: Icons.person,
|
||||
children: [
|
||||
_buildDetailTile(
|
||||
Icons.email_outlined,
|
||||
'Email',
|
||||
isSuperAdmin
|
||||
? data['email']
|
||||
: _maskEmail(
|
||||
data['email']) // Mask email for non-super
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.phone_iphone,
|
||||
'Phone',
|
||||
_formatPhoneNumber(
|
||||
data['phone'].toString(), isSuperAdmin)),
|
||||
_buildDetailTile(Icons.transgender, 'Gender',
|
||||
data['gender'] ?? 'Not specified'),
|
||||
_buildDetailTile(Icons.cake_outlined, 'Birthdate',
|
||||
data['birthdate'] ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Ride Statistics Card ---
|
||||
_buildInfoCard(
|
||||
title: 'Performance & Stats',
|
||||
icon: Icons.bar_chart_rounded,
|
||||
children: [
|
||||
_buildDetailTile(Icons.star_rate_rounded, 'Rating',
|
||||
'${data['ratingPassenger'] ?? 0.0} / 5.0',
|
||||
valueColor: Colors.amber[700]),
|
||||
_buildDetailTile(Icons.directions_car_filled_outlined,
|
||||
'Total Rides', data['countPassengerRide']),
|
||||
_buildDetailTile(Icons.cancel_outlined,
|
||||
'Canceled Rides', data['countPassengerCancel'],
|
||||
valueColor: Colors.redAccent),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// --- Action Buttons ---
|
||||
_buildActionButtons(
|
||||
context, controller, data, isSuperAdmin),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
81
lib/views/admin/captain/driver_details_not_active_page.dart
Normal file
81
lib/views/admin/captain/driver_details_not_active_page.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../controller/drivers/driver_not_active_controller.dart';
|
||||
|
||||
class DriverDetailsPage extends StatelessWidget {
|
||||
final String driverId;
|
||||
final DriverController controller = Get.find();
|
||||
|
||||
DriverDetailsPage({super.key, required this.driverId});
|
||||
|
||||
/// Helper function to safely get String values from dynamic map
|
||||
String safeVal(Map d, String key) {
|
||||
final v = d[key];
|
||||
if (v == null || v == false) return '';
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.getDriverDetails(driverId);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Driver Details")),
|
||||
body: GetBuilder<DriverController>(
|
||||
id: 'driverDetails',
|
||||
builder: (c) {
|
||||
if (c.driverDetails.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final d = c.driverDetails['driver'] as Map;
|
||||
final docs = c.driverDetails['documents'] as List;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Name: ${safeVal(d, 'first_name')} ${safeVal(d, 'last_name')}"),
|
||||
Text("Phone: ${safeVal(d, 'phone')}"),
|
||||
Text("Email: ${safeVal(d, 'email')}"),
|
||||
Text("National Number: ${safeVal(d, 'national_number')}"),
|
||||
Text("Gender: ${safeVal(d, 'gender')}"),
|
||||
Text("Birthdate: ${safeVal(d, 'birthdate')}"),
|
||||
Text("Status: ${safeVal(d, 'status')}"),
|
||||
Text("License Type: ${safeVal(d, 'license_type')}"),
|
||||
Text("License Categories: ${safeVal(d, 'license_categories')}"),
|
||||
Text("Issue Date: ${safeVal(d, 'issue_date')}"),
|
||||
Text("Expiry Date: ${safeVal(d, 'expiry_date')}"),
|
||||
Text("Address: ${safeVal(d, 'address')}"),
|
||||
Text("Site: ${safeVal(d, 'site')}"),
|
||||
Text("Employment Type: ${safeVal(d, 'employmentType')}"),
|
||||
Text("Marital Status: ${safeVal(d, 'maritalStatus')}"),
|
||||
const SizedBox(height: 16),
|
||||
const Text("Documents:",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
...docs.map((doc) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(safeVal(doc, "doc_type")),
|
||||
const SizedBox(height: 4),
|
||||
Image.network(
|
||||
safeVal(doc, "link"),
|
||||
height: 200,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (ctx, err, st) =>
|
||||
const Icon(Icons.broken_image),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/captain_admin_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import 'captain_details.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
|
||||
GetBuilder<CaptainAdminController> formSearchCaptain() {
|
||||
// DbSql sql = DbSql.instance;
|
||||
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();
|
||||
class FormCaptain extends StatefulWidget {
|
||||
const FormCaptain({super.key});
|
||||
|
||||
Get.defaultDialog(
|
||||
title: controller.captain['message'][0]
|
||||
['email'],
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Name is ${controller.captain['message'][0]['first_name']} ${controller.captain['message'][0]['last_name']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'phone is ${controller.captain['message'][0]['phone']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Go To Details'.tr,
|
||||
onPressed: () {
|
||||
Get.to(
|
||||
() => const CaptainsDetailsPage(),
|
||||
arguments: {
|
||||
'data': controller
|
||||
.captain['message'][0],
|
||||
});
|
||||
}));
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.search)),
|
||||
hintText: 'Search for Passenger'.tr,
|
||||
hintStyle: AppStyle.title,
|
||||
hintMaxLines: 1,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () async {
|
||||
controller.captainController.clear();
|
||||
// controller.clearPlaces();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Colors.red[300],
|
||||
),
|
||||
),
|
||||
),
|
||||
controller: controller.captainController,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
@override
|
||||
State<FormCaptain> createState() => _FormCaptainState();
|
||||
}
|
||||
|
||||
class _FormCaptainState extends State<FormCaptain> {
|
||||
final CaptainAdminController controller = Get.find();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
late TextEditingController firstNameController;
|
||||
late TextEditingController lastNameController;
|
||||
late TextEditingController phoneController;
|
||||
|
||||
bool isEditMode = false;
|
||||
Map<String, dynamic>? captainData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (Get.arguments != null && Get.arguments['isEditMode'] == true) {
|
||||
isEditMode = true;
|
||||
captainData = Get.arguments['captainData'];
|
||||
firstNameController =
|
||||
TextEditingController(text: captainData?['first_name'] ?? '');
|
||||
lastNameController =
|
||||
TextEditingController(text: captainData?['last_name'] ?? '');
|
||||
phoneController =
|
||||
TextEditingController(text: captainData?['phone'] ?? '');
|
||||
} else {
|
||||
firstNameController = TextEditingController();
|
||||
lastNameController = TextEditingController();
|
||||
phoneController = TextEditingController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
firstNameController.dispose();
|
||||
lastNameController.dispose();
|
||||
phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveForm() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Create a map of the updated data
|
||||
Map<String, dynamic> updatedData = {
|
||||
'id': captainData?['id'], // Important for the WHERE clause in SQL
|
||||
// 'first_name': firstNameController.text,
|
||||
// 'last_name': lastNameController.text,
|
||||
'phone': phoneController.text,
|
||||
};
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.updateDriverFromAdmin, payload: updatedData);
|
||||
|
||||
if (res != 'failure') {
|
||||
print('Updating data: $updatedData');
|
||||
|
||||
Get.back(); // Go back after saving
|
||||
Get.snackbar('Success', 'Captain data updated successfully!');
|
||||
}
|
||||
// controller.updateCaptain(updatedData);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MyScafolld(
|
||||
title: 'Edit Captain'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Padding(
|
||||
padding: 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
54
lib/views/admin/captain/syrian_driver_not_active.dart
Normal file
54
lib/views/admin/captain/syrian_driver_not_active.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
import 'package:sefer_admin1/constant/box_name.dart';
|
||||
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../constant/char_map.dart';
|
||||
|
||||
import '../../../controller/drivers/driver_not_active_controller.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import 'driver_details_not_active_page.dart';
|
||||
|
||||
class DriversPendingPage extends StatelessWidget {
|
||||
final DriverController controller = Get.put(DriverController());
|
||||
|
||||
DriversPendingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
controller.getDriversPending();
|
||||
Log.print(
|
||||
': ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}');
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Drivers Pending")),
|
||||
body: GetBuilder<DriverController>(
|
||||
id: 'drivers',
|
||||
builder: (c) {
|
||||
if (c.drivers.isEmpty) {
|
||||
return Center(
|
||||
child: const Text('no drivers found yet',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: c.drivers.length,
|
||||
itemBuilder: (ctx, i) {
|
||||
final d = c.drivers[i];
|
||||
return ListTile(
|
||||
title: Text(d["first_name"] + d['last_name'] ?? ""),
|
||||
subtitle: Text(d["phone"] ?? ""),
|
||||
onTap: () {
|
||||
Get.to(() => DriverDetailsPage(driverId: d["id"].toString()));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
219
lib/views/admin/drivers/driver_gift_check_page.dart
Normal file
219
lib/views/admin/drivers/driver_gift_check_page.dart
Normal file
@@ -0,0 +1,219 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||
import 'package:sefer_admin1/controller/functions/wallet.dart'; // تأكد من المسار
|
||||
|
||||
// --- Controller: المسؤول عن المنطق (البحث، الفحص، الإضافة) ---
|
||||
class DriverGiftCheckerController extends GetxController {
|
||||
// للتحكم في حقل النص
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
|
||||
// لعرض النتائج وحالة التحميل
|
||||
var statusLog = "".obs;
|
||||
var isLoading = false.obs;
|
||||
|
||||
// قائمة السائقين (سنقوم بتحميلها للبحث عن الـ ID)
|
||||
List<dynamic> driversCache = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// fetchDriverCache(); // تحميل البيانات عند فتح الصفحة
|
||||
}
|
||||
|
||||
// 1. تحميل قائمة السائقين لاستخراج الـ ID منها
|
||||
Future<void> fetchDriverCache() async {
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link:
|
||||
'https://api.intaleq.xyz/intaleq/Admin/driver/getDriverGiftPayment.php',
|
||||
payload: {'phone': phoneController.text.trim()},
|
||||
);
|
||||
// print('response: ${response}');
|
||||
|
||||
if (response != 'failure') {
|
||||
driversCache = (response['message']);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error loading cache: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// --- الدالة الرئيسية التي تنفذ العملية المطلوبة ---
|
||||
Future<void> processDriverGift() async {
|
||||
String phoneInput = phoneController.text.trim();
|
||||
|
||||
if (phoneInput.isEmpty) {
|
||||
Get.snackbar("تنبيه", "يرجى إدخال رقم الهاتف",
|
||||
backgroundColor: Colors.orange);
|
||||
return;
|
||||
}
|
||||
await fetchDriverCache();
|
||||
isLoading.value = true;
|
||||
statusLog.value = "جاري البحث عن السائق...";
|
||||
|
||||
try {
|
||||
// الخطوة 1: استخراج الـ ID بناءً على رقم الهاتف
|
||||
var driver = driversCache.firstWhere(
|
||||
(d) => d['phone'].toString().contains(phoneInput),
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (driver == null) {
|
||||
statusLog.value = "❌ لم يتم العثور على سائق بهذا الرقم في الكاش.";
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
String driverId = driver['id'].toString();
|
||||
String driverName = driver['name_arabic'] ?? 'بدون اسم';
|
||||
|
||||
statusLog.value =
|
||||
"✅ تم العثور على السائق: $driverName (ID: $driverId)\nجاري فحص رصيد الهدايا...";
|
||||
|
||||
// الخطوة 2: فحص السيرفر هل الهدية موجودة؟
|
||||
// bool hasGift = await _checkIfGiftExistsOnServer(driverId);
|
||||
|
||||
// if (hasGift) {
|
||||
// statusLog.value +=
|
||||
// "\n⚠️ هذا السائق لديه هدية الافتتاح (30,000) مسبقاً. لم يتم اتخاذ إجراء.";
|
||||
// } else {
|
||||
// الخطوة 3: إضافة الهدية
|
||||
statusLog.value += "\n🎁 الهدية غير موجودة. جاري الإضافة...";
|
||||
await _addGiftToDriver(driverId, phoneInput, "30000");
|
||||
// }
|
||||
} catch (e) {
|
||||
statusLog.value = "حدث خطأ غير متوقع: $e";
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// دالة إضافة الهدية باستخدام WalletController الموجود عندك
|
||||
Future<void> _addGiftToDriver(
|
||||
String driverId, String phone, String amount) async {
|
||||
final wallet = Get.put(WalletController());
|
||||
|
||||
// استخدام الدالة الموجودة في نظامك
|
||||
await wallet.addDrivergift3000('new driver', driverId, amount, phone);
|
||||
|
||||
// statusLog.value += "\n✅ تمت إضافة مبلغ $amount ل.س بنجاح!";
|
||||
|
||||
// إضافة تنبيه مرئي
|
||||
// Get.snackbar("تم بنجاح", "تمت إضافة هدية الافتتاح للسائق",
|
||||
// backgroundColor: Colors.green, colorText: Colors.white);
|
||||
}
|
||||
}
|
||||
|
||||
// --- View: واجهة المستخدم ---
|
||||
class DriverGiftCheckPage extends StatelessWidget {
|
||||
const DriverGiftCheckPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حقن الكنترولر
|
||||
final controller = Get.put(DriverGiftCheckerController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8FAFC),
|
||||
appBar: AppBar(
|
||||
title: const Text("فحص ومنح هدية الافتتاح",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: const Color(0xFF0F172A), // نفس لون الهيدر السابق
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// كارد الإدخال
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 10)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.card_giftcard,
|
||||
size: 50, color: Colors.amber),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"أدخل رقم الهاتف للتحقق",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// حقل الإدخال
|
||||
TextField(
|
||||
controller: controller.phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'مثال: 0912345678',
|
||||
prefixIcon: const Icon(Icons.phone),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// زر التنفيذ
|
||||
Obx(() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: () => controller.processDriverGift(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: controller.isLoading.value
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white)
|
||||
: const Text("تحقق ومنح الهدية (30,000)",
|
||||
style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// منطقة عرض النتائج (Log)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Obx(() => Text(
|
||||
controller.statusLog.value.isEmpty
|
||||
? "بانتظار العملية..."
|
||||
: controller.statusLog.value,
|
||||
style: const TextStyle(
|
||||
color: Colors.greenAccent,
|
||||
fontFamily: 'monospace',
|
||||
height: 1.5),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +1,654 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_admin1/constant/links.dart';
|
||||
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||
import 'package:sefer_admin1/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:get_storage/get_storage.dart'; // Ensure get_storage is in pubspec.yaml
|
||||
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';
|
||||
import 'alexandria.dart';
|
||||
import 'giza.dart';
|
||||
// --- New Controller to handle the specific JSON URL ---
|
||||
class DriverCacheController extends GetxController {
|
||||
List<dynamic> drivers = [];
|
||||
bool isLoading = false;
|
||||
String lastUpdated = '';
|
||||
String searchQuery = ''; // Search query state
|
||||
|
||||
class DriverTheBest extends StatelessWidget {
|
||||
const DriverTheBest({super.key});
|
||||
// Storage for paid drivers
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(Driverthebest(), permanent: true);
|
||||
return MyScafolld(
|
||||
title: 'Best Drivers'.tr,
|
||||
body: [
|
||||
GetBuilder<Driverthebest>(builder: (driverthebest) {
|
||||
return driverthebest.driver.isNotEmpty
|
||||
? Column(
|
||||
// Put the new controller
|
||||
final controller = Get.put(DriverCacheController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8FAFC), // slate-50 background
|
||||
body: SafeArea(
|
||||
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: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Giza',
|
||||
onPressed: () {
|
||||
Get.to(() => DriverTheBestGiza());
|
||||
}),
|
||||
MyElevatedButton(
|
||||
title: 'Alexandria',
|
||||
onPressed: () {
|
||||
Get.to(() => DriverTheBestAlexandria());
|
||||
}),
|
||||
],
|
||||
),
|
||||
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
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.local_taxi,
|
||||
color: Colors.yellow, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Best Drivers Dashboard'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
title:
|
||||
Text((driver['name_arabic']) ?? 'Unknown Name'),
|
||||
subtitle:
|
||||
Text('Phone: ${(driver['phone']) ?? 'N/A'}'),
|
||||
trailing: IconButton(
|
||||
onPressed: () async {
|
||||
Get.defaultDialog(
|
||||
title:
|
||||
'are you sure to pay to this driver gift'
|
||||
.tr,
|
||||
middleText: '',
|
||||
onConfirm: () async {
|
||||
final wallet =
|
||||
Get.put(WalletController());
|
||||
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),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time,
|
||||
color: Colors.grey, size: 12),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
ctrl.lastUpdated.isNotEmpty
|
||||
? 'Updated: ${ctrl.lastUpdated}'
|
||||
: 'Data Live',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[400], fontSize: 12),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// 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.'),
|
||||
);
|
||||
})
|
||||
],
|
||||
isleading: true,
|
||||
),
|
||||
),
|
||||
|
||||
// Status Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: statusColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Progress Bar
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Performance",
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey[600])),
|
||||
Text("${hours.toStringAsFixed(2)} hrs",
|
||||
style: const TextStyle(
|
||||
fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.grey[100],
|
||||
color: statusColor,
|
||||
minHeight: 6,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Actions Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// Pay Gift Button (The specific request)
|
||||
isPaid
|
||||
? const Text("Payment Completed",
|
||||
style: TextStyle(
|
||||
color: Colors.teal, fontWeight: FontWeight.bold))
|
||||
: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_showPayDialog(driver, controller);
|
||||
},
|
||||
icon: const Icon(Icons.card_giftcard, size: 16),
|
||||
label: Text("Pay Gift".tr),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo, // Dark blue/purple
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPayDialog(Map driver, DriverCacheController controller) {
|
||||
// Check for valid ID immediately
|
||||
String driverId = driver['driver_id']?.toString() ?? '';
|
||||
String phone = driver['phone']?.toString() ?? '';
|
||||
if (driverId.isEmpty || driverId == 'null') {
|
||||
Get.snackbar("Error", "Cannot pay driver with missing ID",
|
||||
backgroundColor: Colors.red, colorText: Colors.white);
|
||||
return;
|
||||
}
|
||||
|
||||
// Controller for the Amount Field
|
||||
final TextEditingController amountController =
|
||||
TextEditingController(text: '50000');
|
||||
|
||||
Get.defaultDialog(
|
||||
title: 'Confirm Payment',
|
||||
titleStyle: const TextStyle(
|
||||
color: Color(0xFF0F172A), fontWeight: FontWeight.bold),
|
||||
content: Column(
|
||||
children: [
|
||||
const Icon(Icons.wallet_giftcard, size: 50, color: Colors.indigo),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Sending gift to ${driver['name_arabic']}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
// Amount Field
|
||||
TextFormField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Amount (SYP)',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textConfirm: 'Pay Now',
|
||||
confirmTextColor: Colors.white,
|
||||
buttonColor: Colors.indigo,
|
||||
onConfirm: () async {
|
||||
final wallet = Get.put(WalletController());
|
||||
|
||||
// Get amount from field
|
||||
String amount = amountController.text.trim();
|
||||
if (amount.isEmpty) amount = '0';
|
||||
|
||||
// String driverToken = driver['token'] ?? '';
|
||||
|
||||
// 1. Add Payment
|
||||
await wallet.addDriverWallet('gift_connect', driverId, amount, phone);
|
||||
|
||||
// 2. Add to Sefer Wallet
|
||||
//await wallet.addSeferWallet(amount, driverId);
|
||||
|
||||
// 3. Delete Record via CRUD
|
||||
// await CRUD()
|
||||
// .post(link: AppLink.deleteRecord, payload: {'driver_id': driverId});
|
||||
|
||||
// 4. UI Update & Storage
|
||||
// Mark as paid instead of removing completely, so we can see the color change
|
||||
controller.markAsPaid(driverId);
|
||||
|
||||
Get.back(); // Close Dialog
|
||||
|
||||
Get.snackbar("Success", "Payment of $amount EGP sent to driver",
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM);
|
||||
},
|
||||
textCancel: 'Cancel',
|
||||
onCancel: () => Get.back(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
448
lib/views/admin/drivers/driver_tracker_screen.dart
Normal file
448
lib/views/admin/drivers/driver_tracker_screen.dart
Normal file
@@ -0,0 +1,448 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:url_launcher/url_launcher.dart'; // ضروري من أجل الاتصال
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class IntaleqTrackerScreen extends StatefulWidget {
|
||||
const IntaleqTrackerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<IntaleqTrackerScreen> createState() => _IntaleqTrackerScreenState();
|
||||
}
|
||||
|
||||
class _IntaleqTrackerScreenState extends State<IntaleqTrackerScreen> {
|
||||
// === Map Controller ===
|
||||
final MapController _mapController = MapController();
|
||||
List<Marker> _markers = [];
|
||||
|
||||
// === State Variables ===
|
||||
bool isLiveMode = true;
|
||||
bool isLoading = false;
|
||||
String lastUpdated = "جاري التحميل...";
|
||||
|
||||
// === Counters ===
|
||||
int liveCount = 0;
|
||||
int dayCount = 0;
|
||||
Timer? _timer;
|
||||
|
||||
// === Admin Info ===
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool get isSuperAdmin =>
|
||||
myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
// === URLs ===
|
||||
final String _baseDir = "https://api.intaleq.xyz/intaleq/ride/location/";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchData();
|
||||
|
||||
// === تعديل 1: التحديث كل 5 دقائق بدلاً من 15 ثانية ===
|
||||
_timer = Timer.periodic(const Duration(minutes: 5), (timer) {
|
||||
if (mounted) fetchData();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_mapController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// === دالة إجراء الاتصال ===
|
||||
Future<void> _makePhoneCall(String phoneNumber) async {
|
||||
final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber);
|
||||
if (await canLaunchUrl(launchUri)) {
|
||||
await launchUrl(launchUri);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("لا يمكن إجراء الاتصال لهذا الرقم")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// === Fetch Data Function ===
|
||||
Future<void> fetchData() async {
|
||||
if (!mounted) return;
|
||||
setState(() => isLoading = true);
|
||||
|
||||
try {
|
||||
// 1. طلب التحديث من PHP
|
||||
String updateUrl =
|
||||
"${_baseDir}getUpdatedLocationForAdmin.php?mode=${isLiveMode ? 'live' : 'day'}";
|
||||
await http.get(Uri.parse(updateUrl));
|
||||
|
||||
String v = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
// === Live Data ===
|
||||
final responseLive =
|
||||
await http.get(Uri.parse("${_baseDir}locations_live.json?v=$v"));
|
||||
if (responseLive.statusCode == 200) {
|
||||
final data = json.decode(responseLive.body);
|
||||
List drivers = (data is Map && data.containsKey('drivers'))
|
||||
? data['drivers']
|
||||
: data;
|
||||
|
||||
setState(() {
|
||||
liveCount = drivers.length;
|
||||
if (isLiveMode) _buildMarkers(drivers);
|
||||
});
|
||||
}
|
||||
|
||||
// === Day Data ===
|
||||
final responseDay =
|
||||
await http.get(Uri.parse("${_baseDir}locations_day.json?v=$v"));
|
||||
if (responseDay.statusCode == 200) {
|
||||
final data = json.decode(responseDay.body);
|
||||
List drivers = (data is Map && data.containsKey('drivers'))
|
||||
? data['drivers']
|
||||
: data;
|
||||
|
||||
setState(() {
|
||||
dayCount = drivers.length;
|
||||
if (!isLiveMode) _buildMarkers(drivers);
|
||||
});
|
||||
}
|
||||
|
||||
setState(() {
|
||||
lastUpdated = DateTime.now().toString().substring(11, 19);
|
||||
});
|
||||
} catch (e) {
|
||||
print("Exception: $e");
|
||||
setState(() => lastUpdated = "خطأ في الاتصال");
|
||||
} finally {
|
||||
if (mounted) setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
// === Build Markers ===
|
||||
void _buildMarkers(List<dynamic> drivers) {
|
||||
List<Marker> newMarkers = [];
|
||||
|
||||
for (var d in drivers) {
|
||||
double lat = double.tryParse((d['lat'] ?? "0").toString()) ?? 0.0;
|
||||
double lon = double.tryParse((d['lon'] ?? "0").toString()) ?? 0.0;
|
||||
double heading = double.tryParse((d['heading'] ?? "0").toString()) ?? 0.0;
|
||||
|
||||
String id = (d['id'] ?? "Unknown").toString();
|
||||
String speed = (d['speed'] ?? "0").toString();
|
||||
String name = (d['name'] ?? "كابتن").toString();
|
||||
String phone = (d['phone'] ?? "").toString();
|
||||
String completed = (d['completed'] ?? "0").toString();
|
||||
String cancelled = (d['cancelled'] ?? "0").toString();
|
||||
|
||||
if (lat != 0 && lon != 0) {
|
||||
newMarkers.add(
|
||||
Marker(
|
||||
point: LatLng(lat, lon),
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_showDriverInfoDialog(
|
||||
driverId: id,
|
||||
name: name,
|
||||
phone: phone,
|
||||
speed: speed,
|
||||
heading: heading,
|
||||
completed: completed,
|
||||
cancelled: cancelled,
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: const [
|
||||
BoxShadow(blurRadius: 3, color: Colors.black26)
|
||||
]),
|
||||
),
|
||||
Transform.rotate(
|
||||
angle: heading * (math.pi / 180),
|
||||
child: Icon(
|
||||
Icons.navigation,
|
||||
color: isLiveMode
|
||||
? const Color(0xFF27AE60)
|
||||
: const Color(0xFF2980B9),
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_markers = newMarkers;
|
||||
});
|
||||
}
|
||||
|
||||
// === Dialog Function ===
|
||||
void _showDriverInfoDialog({
|
||||
required String driverId,
|
||||
required String name,
|
||||
required String phone,
|
||||
required String speed,
|
||||
required double heading,
|
||||
required String completed,
|
||||
required String cancelled,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
backgroundColor: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text("بيانات الكابتن",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2C3E50))),
|
||||
const Divider(thickness: 1, height: 25),
|
||||
_infoRow(Icons.person, "الاسم", name),
|
||||
_infoRow(Icons.badge, "المعرف (ID)", driverId),
|
||||
_infoRow(Icons.speed, "السرعة", "$speed كم/س"),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade200)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_statItem("مكتملة", completed, Colors.green),
|
||||
Container(
|
||||
width: 1, height: 30, color: Colors.grey.shade300),
|
||||
_statItem("ملغاة", cancelled, Colors.red),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// === تعديل 2: جعل رقم الهاتف قابلاً للنقر ===
|
||||
if (isSuperAdmin) ...[
|
||||
const SizedBox(height: 15),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (phone.isNotEmpty) _makePhoneCall(phone);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3CD),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFFFEEBA))),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _infoRow(Icons.phone, "الهاتف", phone,
|
||||
isPrivate: true)),
|
||||
const SizedBox(width: 5),
|
||||
const Icon(Icons.call, color: Colors.green, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2C3E50),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("إغلاق"),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper Widgets
|
||||
Widget _infoRow(IconData icon, String label, String value,
|
||||
{bool isPrivate = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 20,
|
||||
color: isPrivate ? Colors.orange[800] : Colors.grey[600]),
|
||||
const SizedBox(width: 8),
|
||||
Text("$label: ",
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
Expanded(
|
||||
child: Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight:
|
||||
isPrivate ? FontWeight.bold : FontWeight.normal),
|
||||
textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statItem(String label, String val, Color color) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(val,
|
||||
style: TextStyle(
|
||||
color: color, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text(label, style: const TextStyle(fontSize: 11, color: Colors.grey)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("نظام تتبع الكباتن"),
|
||||
backgroundColor: const Color(0xFF2C3E50),
|
||||
foregroundColor: Colors.white),
|
||||
body: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: const MapOptions(
|
||||
initialCenter: LatLng(33.513, 36.276), initialZoom: 10.0),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.tripz.app'),
|
||||
MarkerLayer(markers: _markers),
|
||||
],
|
||||
),
|
||||
|
||||
// === Dashboard ===
|
||||
Positioned(
|
||||
top: 20,
|
||||
right: 15,
|
||||
child: Container(
|
||||
width: 260,
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black12, blurRadius: 10)
|
||||
]),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Text("لوحة التحكم",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const Divider(),
|
||||
|
||||
// أزرار التبديل
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _modeBtn("أرشيف اليوم", !isLiveMode, () {
|
||||
setState(() => isLiveMode = false);
|
||||
fetchData();
|
||||
})),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _modeBtn("مباشر", isLiveMode, () {
|
||||
setState(() => isLiveMode = true);
|
||||
fetchData();
|
||||
})),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// === عرض العدادين معاً ===
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("$liveCount",
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF27AE60),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14)),
|
||||
const Text("نشط الآن (مباشر):",
|
||||
style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("$dayCount",
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF2980B9),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14)),
|
||||
const Text("إجمالي اليوم:",
|
||||
style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
Text(isLoading ? "جاري التحديث..." : "تحديث: $lastUpdated",
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : fetchData,
|
||||
child: const Text("تحديث البيانات")))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _modeBtn(String title, bool active, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: active ? const Color(0xFF3498DB) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFF3498DB))),
|
||||
child: Text(title,
|
||||
style: TextStyle(
|
||||
color: active ? Colors.white : const Color(0xFF3498DB),
|
||||
fontWeight: active ? FontWeight.bold : FontWeight.normal)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
557
lib/views/admin/drivers/monitor_ride.dart
Normal file
557
lib/views/admin/drivers/monitor_ride.dart
Normal file
@@ -0,0 +1,557 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
// Keep your specific imports
|
||||
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||
import 'package:sefer_admin1/print.dart';
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 1. DATA MODELS
|
||||
/// --------------------------------------------------------------------------
|
||||
|
||||
class DriverLocation {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double speed;
|
||||
final double heading;
|
||||
final String updatedAt;
|
||||
|
||||
DriverLocation({
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.speed,
|
||||
required this.heading,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory DriverLocation.fromJson(Map<String, dynamic> json) {
|
||||
return DriverLocation(
|
||||
latitude: double.tryParse(json['latitude'].toString()) ?? 0.0,
|
||||
longitude: double.tryParse(json['longitude'].toString()) ?? 0.0,
|
||||
speed: double.tryParse(json['speed'].toString()) ?? 0.0,
|
||||
heading: double.tryParse(json['heading'].toString()) ?? 0.0,
|
||||
updatedAt: json['updated_at'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 2. GETX CONTROLLER
|
||||
/// --------------------------------------------------------------------------
|
||||
|
||||
class RideMonitorController extends GetxController {
|
||||
// CONFIGURATION
|
||||
final String apiUrl =
|
||||
"https://api.intaleq.xyz/intaleq/Admin/rides/monitorRide.php";
|
||||
|
||||
// INPUT CONTROLLERS
|
||||
final TextEditingController phoneInputController = TextEditingController();
|
||||
|
||||
// OBSERVABLES
|
||||
var isTracking = false.obs;
|
||||
var isLoading = false.obs;
|
||||
var hasError = false.obs;
|
||||
var errorMessage = ''.obs;
|
||||
|
||||
// Driver & Ride Data
|
||||
var driverLocation = Rxn<DriverLocation>();
|
||||
var driverName = "Unknown Driver".obs;
|
||||
var rideStatus = "Waiting...".obs;
|
||||
|
||||
// Route Data
|
||||
var startPoint = Rxn<LatLng>();
|
||||
var endPoint = Rxn<LatLng>();
|
||||
var routePolyline = <LatLng>[].obs; // List of points for the line
|
||||
|
||||
// Map Variables
|
||||
final MapController mapController = MapController();
|
||||
Timer? _timer;
|
||||
bool _isFirstLoad = true; // To trigger auto-fit bounds only on first success
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_timer?.cancel();
|
||||
phoneInputController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// --- ACTIONS ---
|
||||
|
||||
void startSearch() {
|
||||
if (phoneInputController.text.trim().isEmpty) {
|
||||
Get.snackbar("Error", "Please enter a phone number");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
hasError.value = false;
|
||||
errorMessage.value = '';
|
||||
driverLocation.value = null;
|
||||
startPoint.value = null;
|
||||
endPoint.value = null;
|
||||
routePolyline.clear();
|
||||
driverName.value = "Loading...";
|
||||
rideStatus.value = "Loading...";
|
||||
_isFirstLoad = true;
|
||||
|
||||
// Switch UI
|
||||
isTracking.value = true;
|
||||
isLoading.value = true;
|
||||
|
||||
// Start fetching
|
||||
fetchRideData();
|
||||
|
||||
// Start Polling
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
||||
fetchRideData();
|
||||
});
|
||||
}
|
||||
|
||||
void stopTracking() {
|
||||
_timer?.cancel();
|
||||
isTracking.value = false;
|
||||
isLoading.value = false;
|
||||
phoneInputController.clear();
|
||||
}
|
||||
|
||||
Future<void> fetchRideData() async {
|
||||
final phone = phoneInputController.text.trim();
|
||||
if (phone.isEmpty) return;
|
||||
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: apiUrl,
|
||||
payload: {"phone": phone},
|
||||
);
|
||||
|
||||
// Log.print('response: ${response}');
|
||||
|
||||
if (response != 'failure') {
|
||||
final jsonResponse = response;
|
||||
|
||||
if ((jsonResponse['message'] != null &&
|
||||
jsonResponse['message'] != 'failure') ||
|
||||
jsonResponse['status'] == 'success') {
|
||||
final data =
|
||||
jsonResponse['message'] ?? jsonResponse['data'] ?? jsonResponse;
|
||||
|
||||
// 1. Parse Driver Info
|
||||
if (data['driver_details'] != null) {
|
||||
driverName.value = data['driver_details']['fullname'] ?? "Unknown";
|
||||
}
|
||||
|
||||
// 2. Parse Ride Info & Route
|
||||
if (data['ride_details'] != null) {
|
||||
rideStatus.value = data['ride_details']['status'] ?? "Unknown";
|
||||
|
||||
// Parse Start/End Locations (Format: "lat,lng")
|
||||
String? startStr = data['ride_details']['start_location'];
|
||||
String? endStr = data['ride_details']['end_location'];
|
||||
|
||||
LatLng? s = _parseLatLngString(startStr);
|
||||
LatLng? e = _parseLatLngString(endStr);
|
||||
|
||||
if (s != null && e != null) {
|
||||
startPoint.value = s;
|
||||
endPoint.value = e;
|
||||
routePolyline.value = [s, e]; // Straight line for now
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Parse Live Location
|
||||
final locData = data['driver_location'];
|
||||
if (locData is Map<String, dynamic>) {
|
||||
final newLocation = DriverLocation.fromJson(locData);
|
||||
driverLocation.value = newLocation;
|
||||
|
||||
// 4. Update Camera Bounds
|
||||
_updateMapBounds();
|
||||
} else {
|
||||
// Even if no live driver, we might want to show the route
|
||||
if (startPoint.value != null && endPoint.value != null) {
|
||||
_updateMapBounds();
|
||||
}
|
||||
print("No live location coordinates.");
|
||||
}
|
||||
|
||||
hasError.value = false;
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = jsonResponse['message'] ??
|
||||
"Phone number not found or no active ride.";
|
||||
}
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = "Connection Failed";
|
||||
}
|
||||
} catch (e) {
|
||||
print("Polling Error: $e");
|
||||
if (isLoading.value) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = e.toString();
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse "lat,lng" string
|
||||
LatLng? _parseLatLngString(String? str) {
|
||||
if (str == null || !str.contains(',')) return null;
|
||||
try {
|
||||
final parts = str.split(',');
|
||||
final lat = double.parse(parts[0].trim());
|
||||
final lng = double.parse(parts[1].trim());
|
||||
return LatLng(lat, lng);
|
||||
} catch (e) {
|
||||
print("Error parsing location string '$str': $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Logic to fit start, end, and driver on screen
|
||||
void _updateMapBounds() {
|
||||
// Only auto-fit on the first successful load to avoid fighting user pan/zoom
|
||||
if (!_isFirstLoad) return;
|
||||
|
||||
List<LatLng> pointsToFit = [];
|
||||
|
||||
if (startPoint.value != null) pointsToFit.add(startPoint.value!);
|
||||
if (endPoint.value != null) pointsToFit.add(endPoint.value!);
|
||||
if (driverLocation.value != null) {
|
||||
pointsToFit.add(LatLng(
|
||||
driverLocation.value!.latitude, driverLocation.value!.longitude));
|
||||
}
|
||||
|
||||
if (pointsToFit.isNotEmpty) {
|
||||
try {
|
||||
final bounds = LatLngBounds.fromPoints(pointsToFit);
|
||||
mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding:
|
||||
const EdgeInsets.all(80.0), // Padding so markers aren't on edge
|
||||
),
|
||||
);
|
||||
_isFirstLoad = false; // Disable auto-fit after initial success
|
||||
} catch (e) {
|
||||
print("Map Controller not ready yet: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// 3. UI SCREEN
|
||||
/// --------------------------------------------------------------------------
|
||||
|
||||
class RideMonitorScreen extends StatelessWidget {
|
||||
const RideMonitorScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final RideMonitorController controller = Get.put(RideMonitorController());
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Admin Ride Monitor"),
|
||||
backgroundColor: Colors.blueAccent,
|
||||
foregroundColor: Colors.white,
|
||||
actions: [
|
||||
Obx(() => controller.isTracking.value
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.stopTracking,
|
||||
tooltip: "Stop Tracking",
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
if (!controller.isTracking.value) {
|
||||
return _buildSearchForm(context, controller);
|
||||
}
|
||||
return _buildMapTrackingView(controller);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchForm(
|
||||
BuildContext context, RideMonitorController controller) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.map_outlined, size: 80, color: Colors.blueAccent),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"Track Active Ride",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"Enter Driver or Passenger Phone Number",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
TextField(
|
||||
controller: controller.phoneInputController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Phone Number",
|
||||
hintText: "e.g. 9639...",
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||
prefixIcon: const Icon(Icons.phone),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.startSearch,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueAccent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: const Text("Start Monitoring",
|
||||
style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMapTrackingView(RideMonitorController controller) {
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: controller.mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: const LatLng(30.0444, 31.2357),
|
||||
initialZoom: 12.0,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.sefer.admin',
|
||||
),
|
||||
|
||||
// 1. ROUTE LINE (Polyline)
|
||||
if (controller.routePolyline.isNotEmpty)
|
||||
PolylineLayer(
|
||||
polylines: [
|
||||
Polyline(
|
||||
points: controller.routePolyline.value,
|
||||
strokeWidth: 5.0,
|
||||
color: Colors.blueAccent.withOpacity(0.8),
|
||||
borderStrokeWidth: 2.0,
|
||||
borderColor: Colors.blue[900]!,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 2. START & END MARKERS
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
// Start Point (Green Flag)
|
||||
if (controller.startPoint.value != null)
|
||||
Marker(
|
||||
point: controller.startPoint.value!,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child:
|
||||
const Icon(Icons.flag, color: Colors.green, size: 40),
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
|
||||
// End Point (Red Flag)
|
||||
if (controller.endPoint.value != null)
|
||||
Marker(
|
||||
point: controller.endPoint.value!,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: const Icon(Icons.flag, color: Colors.red, size: 40),
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
|
||||
// Driver Car Marker
|
||||
if (controller.driverLocation.value != null)
|
||||
Marker(
|
||||
point: LatLng(
|
||||
controller.driverLocation.value!.latitude,
|
||||
controller.driverLocation.value!.longitude,
|
||||
),
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Transform.rotate(
|
||||
angle: (controller.driverLocation.value!.heading *
|
||||
(3.14159 / 180)),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.directions_car_filled,
|
||||
color: Colors
|
||||
.black, // Dark car for visibility on blue line
|
||||
size: 35,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
"${controller.driverLocation.value!.speed.toInt()} km",
|
||||
style: const TextStyle(
|
||||
fontSize: 10, fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// LOADING OVERLAY
|
||||
if (controller.isLoading.value &&
|
||||
controller.driverLocation.value == null &&
|
||||
controller.startPoint.value == null)
|
||||
Container(
|
||||
color: Colors.black45,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white)),
|
||||
),
|
||||
|
||||
// ERROR OVERLAY
|
||||
if (controller.hasError.value)
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [
|
||||
BoxShadow(blurRadius: 10, color: Colors.black26)
|
||||
]),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 40),
|
||||
const SizedBox(height: 10),
|
||||
Text(controller.errorMessage.value,
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: controller.stopTracking,
|
||||
child: const Text("Back"))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// INFO CARD
|
||||
if (!controller.hasError.value && !controller.isLoading.value)
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
backgroundColor: Colors.blueAccent,
|
||||
child: Icon(Icons.person, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.driverName.value,
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.circle,
|
||||
size: 10,
|
||||
color:
|
||||
controller.rideStatus.value == 'Begin'
|
||||
? Colors.green
|
||||
: Colors.grey),
|
||||
const SizedBox(width: 5),
|
||||
Text(controller.rideStatus.value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
if (controller.driverLocation.value != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildInfoBadge(Icons.speed,
|
||||
"${controller.driverLocation.value!.speed.toStringAsFixed(1)} km/h"),
|
||||
_buildInfoBadge(
|
||||
Icons.access_time,
|
||||
controller.driverLocation.value!.updatedAt
|
||||
.split(' ')
|
||||
.last),
|
||||
],
|
||||
)
|
||||
else
|
||||
const Text("Connecting to driver...",
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontStyle: FontStyle.italic)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoBadge(IconData icon, String text) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(text,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[800], fontWeight: FontWeight.bold)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
352
lib/views/admin/error/error/error_page.dart
Normal file
352
lib/views/admin/error/error/error_page.dart
Normal file
@@ -0,0 +1,352 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sefer_admin1/constant/colors.dart';
|
||||
import 'package:sefer_admin1/constant/links.dart';
|
||||
import 'package:sefer_admin1/controller/functions/crud.dart';
|
||||
|
||||
class ErrorLog {
|
||||
final String id;
|
||||
final String error;
|
||||
final String userId;
|
||||
final String userType;
|
||||
final String phone;
|
||||
final String createdAt;
|
||||
final String device;
|
||||
final String details;
|
||||
final String status;
|
||||
|
||||
ErrorLog({
|
||||
required this.id,
|
||||
required this.error,
|
||||
required this.userId,
|
||||
required this.userType,
|
||||
required this.phone,
|
||||
required this.createdAt,
|
||||
required this.device,
|
||||
required this.details,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory ErrorLog.fromJson(Map<String, dynamic> j) => ErrorLog(
|
||||
id: (j['id'] ?? '').toString(),
|
||||
error: (j['error'] ?? '').toString(),
|
||||
userId: (j['userId'] ?? '').toString(),
|
||||
userType: (j['userType'] ?? '').toString(),
|
||||
phone: (j['phone'] ?? '').toString(),
|
||||
createdAt: (j['created_at'] ?? '').toString(),
|
||||
device: (j['device'] ?? '').toString(),
|
||||
details: (j['details'] ?? '').toString(),
|
||||
status: (j['status'] ?? '').toString(),
|
||||
);
|
||||
}
|
||||
|
||||
class ErrorListPage extends StatefulWidget {
|
||||
const ErrorListPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ErrorListPage> createState() => _ErrorListPageState();
|
||||
}
|
||||
|
||||
class _ErrorListPageState extends State<ErrorListPage> {
|
||||
static String baseUrl = '${AppLink.server}/Admin/error';
|
||||
static const String listEndpoint = "error_list_last20.php";
|
||||
static const String searchEndpoint = "error_search_by_phone.php";
|
||||
|
||||
final TextEditingController _phoneCtrl = TextEditingController();
|
||||
bool _loading = false;
|
||||
String? _errorMsg;
|
||||
List<ErrorLog> _items = [];
|
||||
bool _searchMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchLast20();
|
||||
}
|
||||
|
||||
Future<void> _fetchLast20() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_errorMsg = null;
|
||||
_searchMode = false;
|
||||
});
|
||||
try {
|
||||
final res =
|
||||
await CRUD().post(link: "$baseUrl/$listEndpoint", payload: {});
|
||||
final map = (res);
|
||||
if (map['status'] == 'success') {
|
||||
final List data = (map['message'] ?? []) as List;
|
||||
final items = data.map((e) => ErrorLog.fromJson(e)).toList();
|
||||
setState(() {
|
||||
_items = items.cast<ErrorLog>();
|
||||
});
|
||||
} else {
|
||||
setState(() => _errorMsg = map['message']?.toString() ?? 'Failed');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _errorMsg = e.toString());
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _searchByPhone() async {
|
||||
final phone = _phoneCtrl.text.trim();
|
||||
if (phone.isEmpty) {
|
||||
return _fetchLast20();
|
||||
}
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_errorMsg = null;
|
||||
_searchMode = true;
|
||||
});
|
||||
try {
|
||||
final res = await CRUD()
|
||||
.post(link: "$baseUrl/$searchEndpoint", payload: {"phone": phone});
|
||||
final map = (res);
|
||||
if (map['status'] == 'success') {
|
||||
final List data = (map['message'] ?? []) as List;
|
||||
final items = data.map((e) => ErrorLog.fromJson(e)).toList();
|
||||
setState(() {
|
||||
_items = items.cast<ErrorLog>();
|
||||
});
|
||||
} else {
|
||||
setState(() => _errorMsg = map['message']?.toString() ?? 'Failed');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _errorMsg = e.toString());
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _clearSearch() {
|
||||
_phoneCtrl.clear();
|
||||
FocusScope.of(context).unfocus();
|
||||
_fetchLast20();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100],
|
||||
appBar: AppBar(
|
||||
title: const Text('سجل الأخطاء'),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 1.0,
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_SearchBar(
|
||||
controller: _phoneCtrl,
|
||||
onSearch: _searchByPhone,
|
||||
onClear: _clearSearch,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildBody(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_errorMsg != null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
_errorMsg!,
|
||||
style: TextStyle(color: Colors.red[700]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_items.isEmpty) {
|
||||
return const Center(child: Text('لا توجد سجلات'));
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if (_searchMode) {
|
||||
await _searchByPhone();
|
||||
} else {
|
||||
await _fetchLast20();
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final e = _items[index];
|
||||
return _ErrorTile(e);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SearchBar extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final VoidCallback onSearch;
|
||||
final VoidCallback onClear;
|
||||
|
||||
const _SearchBar({
|
||||
required this.controller,
|
||||
required this.onSearch,
|
||||
required this.onClear,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(12),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.phone,
|
||||
onSubmitted: (_) => onSearch(),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'بحث برقم الهاتف...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: onSearch,
|
||||
child: const Text('بحث'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: onClear,
|
||||
child: const Text('مسح'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorTile extends StatelessWidget {
|
||||
final ErrorLog item;
|
||||
const _ErrorTile(this.item);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// تحديد الألوان بناءً على نوع المستخدم
|
||||
Color? typeBgColor;
|
||||
Color? typeTextColor;
|
||||
|
||||
final type = item.userType.toLowerCase();
|
||||
if (type.contains('driver') || type.contains('سائق')) {
|
||||
typeBgColor = Colors.green.shade100;
|
||||
typeTextColor = Colors.green.shade800;
|
||||
} else if (type.contains('passenger') || type.contains('راكب')) {
|
||||
typeBgColor = Colors.amber.shade100; // لون ذهبي/أصفر
|
||||
typeTextColor = Colors.amber.shade900;
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// السطر الأول: نص الخطأ (قابل للنسخ)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
item.error.isEmpty ? '(بدون عنوان)' : item.error,
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
// تم إزالة _StatusPill (ويدجت الحالة New) من هنا
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// تفاصيل مختصرة (قابل للنسخ)
|
||||
if (item.details.isNotEmpty)
|
||||
SelectableText(
|
||||
item.details,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(color: Colors.grey[700]),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// معلومات تقنية
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
_KV('الهاتف', item.phone),
|
||||
_KV('المستخدم', item.userId),
|
||||
// تمرير الألوان المخصصة لنوع المستخدم
|
||||
_KV(
|
||||
'النوع',
|
||||
item.userType,
|
||||
bgColor: typeBgColor,
|
||||
textColor: typeTextColor,
|
||||
),
|
||||
_KV('الجهاز', item.device),
|
||||
_KV('التاريخ', item.createdAt),
|
||||
_KV('ID', item.id),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KV extends StatelessWidget {
|
||||
final String k;
|
||||
final String v;
|
||||
final Color? bgColor;
|
||||
final Color? textColor;
|
||||
|
||||
const _KV(this.k, this.v, {this.bgColor, this.textColor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor ?? Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text.rich(TextSpan(
|
||||
style: TextStyle(fontSize: 11, color: textColor ?? Colors.grey[600]),
|
||||
children: [
|
||||
TextSpan(text: "$k: "),
|
||||
TextSpan(
|
||||
text: v.isEmpty ? '—' : v,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor ?? Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,205 +1,390 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package: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 '../../../controller/admin/passenger_admin_controller.dart';
|
||||
import '../../../main.dart'; // للوصول إلى box
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import '../../widgets/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import 'form_passenger.dart';
|
||||
import 'passenger_details_page.dart';
|
||||
|
||||
class Passengrs extends StatelessWidget {
|
||||
Passengrs({super.key});
|
||||
|
||||
final PassengerAdminController passengerAdminController =
|
||||
Get.put(PassengerAdminController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 1. منطق السوبر أدمن
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
return MyScafolld(
|
||||
title: 'Passengrs'.tr,
|
||||
title: 'Passengers Management'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
GetBuilder<PassengerAdminController>(
|
||||
builder: (passengerAdminController) => Column(
|
||||
children: [
|
||||
passengerAdminController.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Column(
|
||||
children: [
|
||||
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;
|
||||
// استخدام Expanded أو Container بطول الشاشة لتجنب المشاكل
|
||||
SizedBox(
|
||||
height: Get.height, // تأمين مساحة العمل
|
||||
child: GetBuilder<PassengerAdminController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (day == 6) {
|
||||
// Saturday is 6
|
||||
Get.defaultDialog(
|
||||
title:
|
||||
'Add Prize to Gold Passengers',
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Add Points to their wallet as prize'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Form(
|
||||
key:
|
||||
passengerAdminController
|
||||
.formPrizeKey,
|
||||
child: MyTextForm(
|
||||
controller:
|
||||
passengerAdminController
|
||||
.passengerPrizeController,
|
||||
label:
|
||||
'Count of prize'
|
||||
.tr,
|
||||
hint: 'Count of prize'
|
||||
.tr,
|
||||
type: TextInputType
|
||||
.number))
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Add',
|
||||
onPressed: () async {
|
||||
if (passengerAdminController
|
||||
.formPrizeKey
|
||||
.currentState!
|
||||
.validate()) {
|
||||
passengerAdminController
|
||||
.addPassengerPrizeToWalletSecure();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title:
|
||||
'This day is not allowed',
|
||||
titleStyle: AppStyle.title,
|
||||
middleText:
|
||||
'Saturday only Allowed day',
|
||||
middleTextStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
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 Column(
|
||||
children: [
|
||||
// --- قسم الإحصائيات والجوائز (Dashboard) ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildDashboardCard(context, controller),
|
||||
),
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Get.to(const PassengerDetailsPage(),
|
||||
arguments: {
|
||||
'data': user,
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(width: 2)),
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Name : ${(user['first_name'])} ${(user['last_name'])}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Rating : ${user['ratingPassenger']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Count Trip : ${user['countPassengerRide']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'Count Driver Rate : ${user['countDriverRate']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
))
|
||||
// --- عنوان القائمة ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"All Passengers".tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
"${controller.passengersData['message']?.length ?? 0} Users",
|
||||
style:
|
||||
TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// --- قائمة الركاب ---
|
||||
// استخدام Expanded هنا هو الحل الجذري لمكلة Overflow
|
||||
Expanded(
|
||||
child: _buildPassengersList(controller, isSuperAdmin),
|
||||
),
|
||||
|
||||
// مساحة سفلية صغيرة لضمان عدم التصاق القائمة بالحافة
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Container passengerAdmin(PassengerAdminController passengerAdminController,
|
||||
String title, String jsonField) {
|
||||
// --- تصميم بطاقة الإحصائيات (Dashboard) ---
|
||||
Widget _buildDashboardCard(
|
||||
BuildContext context, PassengerAdminController controller) {
|
||||
// جلب العدد بأمان
|
||||
final String countValue = (controller.passengersData['message'] != null &&
|
||||
controller.passengersData['message'].isNotEmpty)
|
||||
? controller.passengersData['message'][0]['countPassenger']
|
||||
?.toString() ??
|
||||
'0'
|
||||
: '0';
|
||||
|
||||
return Container(
|
||||
height: Get.height * .1,
|
||||
decoration: BoxDecoration(border: Border.all(width: 2)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
title.tr,
|
||||
style: AppStyle.title,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.groups_rounded,
|
||||
color: AppColor.primaryColor, size: 30),
|
||||
),
|
||||
Text(
|
||||
passengerAdminController.passengersData['message'][0][jsonField]
|
||||
.toString(),
|
||||
style: AppStyle.title,
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Total Passengers'.tr,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
Text(
|
||||
countValue,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 45,
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.card_giftcard,
|
||||
color: Colors.white, size: 20),
|
||||
label: Text('Add Prize to Gold Passengers'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.yellowColor, // لون ذهبي
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () {
|
||||
_showAddPrizeDialog(controller);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- بناء قائمة الركاب ---
|
||||
Widget _buildPassengersList(
|
||||
PassengerAdminController controller, bool isSuperAdmin) {
|
||||
final List<dynamic> passengers = controller.passengersData['message'] ?? [];
|
||||
|
||||
if (passengers.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,167 +1,458 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/admin/passenger_admin_controller.dart';
|
||||
import '../../../controller/functions/crud.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/my_scafold.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import 'form_passenger.dart';
|
||||
|
||||
class PassengerDetailsPage extends StatelessWidget {
|
||||
const PassengerDetailsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final arguments = Get.arguments;
|
||||
final Map<String, dynamic> data = arguments['data'];
|
||||
var key = Get.find<PassengerAdminController>().formPrizeKey;
|
||||
var titleNotify = Get.find<PassengerAdminController>().titleNotify;
|
||||
var bodyNotify = Get.find<PassengerAdminController>().bodyNotify;
|
||||
final Map<String, dynamic> data = Get.arguments['data'];
|
||||
final controller = Get.find<PassengerAdminController>();
|
||||
|
||||
// 1. Define Super Admin Logic (Same as Captains Page)
|
||||
String myPhone = box.read(BoxName.adminPhone).toString();
|
||||
bool isSuperAdmin = myPhone == '963942542053' || myPhone == '963992952235';
|
||||
|
||||
return MyScafolld(
|
||||
title: data['first_name'],
|
||||
title: 'Passenger Profile'.tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Email is ${data['email']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Phone is ${data['phone']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
'gender is ${data['gender']}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
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)
|
||||
],
|
||||
),
|
||||
// --- Header Section (Avatar & Name) ---
|
||||
_buildHeaderSection(context, data),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// --- Personal Information Card ---
|
||||
_buildInfoCard(
|
||||
title: 'Personal Information',
|
||||
icon: Icons.person,
|
||||
children: [
|
||||
_buildDetailTile(
|
||||
Icons.email_outlined,
|
||||
'Email',
|
||||
isSuperAdmin
|
||||
? data['email']
|
||||
: _maskEmail(data['email']),
|
||||
),
|
||||
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 Passenger ".tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.phone_iphone,
|
||||
'Phone',
|
||||
_formatPhoneNumber(
|
||||
data['phone'].toString(), isSuperAdmin),
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.transgender,
|
||||
'Gender',
|
||||
data['gender'] ?? 'Not specified',
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.cake_outlined,
|
||||
'Birthdate',
|
||||
data['birthdate'] ?? 'N/A',
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.location_on_outlined,
|
||||
'Site',
|
||||
data['site'] ?? 'N/A',
|
||||
),
|
||||
// SOS Phone is critical, usually shown, but we can mask it too if needed
|
||||
_buildDetailTile(
|
||||
Icons.sos,
|
||||
'SOS Phone',
|
||||
data['sosPhone'] ?? 'N/A',
|
||||
valueColor: Colors.redAccent,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Ride Statistics Card ---
|
||||
_buildInfoCard(
|
||||
title: 'Activity & Stats',
|
||||
icon: Icons.bar_chart_rounded,
|
||||
children: [
|
||||
_buildDetailTile(
|
||||
Icons.star_rate_rounded,
|
||||
'Rating',
|
||||
'${data['ratingPassenger'] ?? 0.0}',
|
||||
valueColor: Colors.amber[700],
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.directions_car_filled_outlined,
|
||||
'Total Rides',
|
||||
data['countPassengerRide'],
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.cancel_outlined,
|
||||
'Canceled Rides',
|
||||
data['countPassengerCancel'],
|
||||
valueColor: Colors.redAccent,
|
||||
),
|
||||
_buildDetailTile(
|
||||
Icons.rate_review_outlined,
|
||||
'Feedback Given',
|
||||
data['countFeedback'],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// --- Action Buttons ---
|
||||
_buildActionButtons(
|
||||
context, controller, data, isSuperAdmin),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
697
lib/views/admin/rides/ride_lookup_page.dart
Normal file
697
lib/views/admin/rides/ride_lookup_page.dart
Normal file
@@ -0,0 +1,697 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:url_launcher/url_launcher.dart'; // ضروري للاتصال
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../constant/box_name.dart'; // لتحديد هوية المستخدم الحالي
|
||||
import '../../../main.dart'; // للوصول لـ box
|
||||
|
||||
// ==========================================
|
||||
// 1. MODEL
|
||||
// ==========================================
|
||||
class RideDashboardModel {
|
||||
final String rideId;
|
||||
final String status;
|
||||
final String startLocation;
|
||||
final String endLocation;
|
||||
final String date;
|
||||
final String time;
|
||||
final String price;
|
||||
final String distance;
|
||||
|
||||
final String driverId;
|
||||
final String driverName;
|
||||
final String driverPhone;
|
||||
final String driverCompletedCount;
|
||||
final String driverCanceledCount;
|
||||
|
||||
final String passengerName;
|
||||
final String passengerPhone;
|
||||
final String passengerCompletedCount;
|
||||
|
||||
final String cancelReason;
|
||||
|
||||
RideDashboardModel({
|
||||
required this.rideId,
|
||||
required this.status,
|
||||
required this.startLocation,
|
||||
required this.endLocation,
|
||||
required this.date,
|
||||
required this.time,
|
||||
required this.price,
|
||||
required this.distance,
|
||||
required this.driverId,
|
||||
required this.driverName,
|
||||
required this.driverPhone,
|
||||
required this.driverCompletedCount,
|
||||
required this.driverCanceledCount,
|
||||
required this.passengerName,
|
||||
required this.passengerPhone,
|
||||
required this.passengerCompletedCount,
|
||||
required this.cancelReason,
|
||||
});
|
||||
|
||||
factory RideDashboardModel.fromJson(Map<String, dynamic> json) {
|
||||
return RideDashboardModel(
|
||||
rideId: json['id'].toString(),
|
||||
status: json['status'] ?? '',
|
||||
startLocation: json['start_location'] ?? '',
|
||||
endLocation: json['end_location'] ?? '',
|
||||
date: json['date'] ?? '',
|
||||
time: json['time'] ?? '',
|
||||
price: json['price']?.toString() ?? '0',
|
||||
distance: json['distance']?.toString() ?? '0',
|
||||
driverId: json['driver_id'].toString(),
|
||||
driverName: json['driver_full_name'] ?? 'غير معروف',
|
||||
driverPhone: json['d_phone'] ?? '',
|
||||
driverCompletedCount: (json['d_completed'] ?? 0).toString(),
|
||||
driverCanceledCount: (json['d_canceled'] ?? 0).toString(),
|
||||
passengerName: json['passenger_full_name'] ?? 'غير معروف',
|
||||
passengerPhone: json['p_phone'] ?? '',
|
||||
passengerCompletedCount: (json['p_completed'] ?? 0).toString(),
|
||||
cancelReason: json['cancel_reason'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
LatLng? getStartLatLng() {
|
||||
try {
|
||||
var parts = startLocation.split(',');
|
||||
return LatLng(double.parse(parts[0]), double.parse(parts[1]));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
LatLng? getEndLatLng() {
|
||||
try {
|
||||
var parts = endLocation.split(',');
|
||||
return LatLng(double.parse(parts[0]), double.parse(parts[1]));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. CONTROLLER
|
||||
// ==========================================
|
||||
class RidesListController extends GetxController {
|
||||
var isLoading = false.obs;
|
||||
var allRidesList = <RideDashboardModel>[];
|
||||
var displayedRides = <RideDashboardModel>[].obs;
|
||||
|
||||
TextEditingController searchController = TextEditingController();
|
||||
String currentStatus = 'Begin';
|
||||
|
||||
// === التحقق من صلاحية الأدمن ===
|
||||
// نقرأ رقم الهاتف الحالي المخزن في التطبيق
|
||||
String myPhone = box.read(BoxName.adminPhone)?.toString() ?? '';
|
||||
|
||||
bool get isSuperAdmin {
|
||||
// ضع هنا أرقام هواتف الأدمن المسموح لهم برؤية الأرقام والاتصال
|
||||
return myPhone == '963942542053' || myPhone == '963992952235';
|
||||
}
|
||||
|
||||
final String apiUrl =
|
||||
"https://api.intaleq.xyz/intaleq/Admin/rides/get_rides_by_status.php";
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchRides();
|
||||
}
|
||||
|
||||
void changeTab(String status) {
|
||||
currentStatus = status;
|
||||
searchController.clear();
|
||||
fetchRides();
|
||||
}
|
||||
|
||||
void filterRides(String query) {
|
||||
if (query.isEmpty) {
|
||||
displayedRides.value = allRidesList;
|
||||
} else {
|
||||
displayedRides.value = allRidesList.where((ride) {
|
||||
return ride.driverPhone.contains(query) ||
|
||||
ride.passengerPhone.contains(query) ||
|
||||
ride.driverName.toLowerCase().contains(query.toLowerCase()) ||
|
||||
ride.passengerName.toLowerCase().contains(query.toLowerCase()) ||
|
||||
ride.rideId.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchRides() async {
|
||||
isLoading.value = true;
|
||||
allRidesList.clear();
|
||||
displayedRides.clear();
|
||||
try {
|
||||
var response =
|
||||
await CRUD().post(link: apiUrl, payload: {"status": currentStatus});
|
||||
if (response != 'failure' && response['status'] == 'success') {
|
||||
List<dynamic> data = [];
|
||||
if (response['message'] is List)
|
||||
data = response['message'];
|
||||
else if (response['data'] is List) data = response['data'];
|
||||
|
||||
allRidesList = data.map((e) => RideDashboardModel.fromJson(e)).toList();
|
||||
displayedRides.value = allRidesList;
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching rides: $e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. MAIN DASHBOARD SCREEN
|
||||
// ==========================================
|
||||
class RidesDashboardScreen extends StatelessWidget {
|
||||
const RidesDashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(RidesListController());
|
||||
|
||||
return DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("مراقبة الرحلات"),
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
onTap: (index) {
|
||||
List<String> statuses = ['Begin', 'New', 'Completed', 'Canceled'];
|
||||
controller.changeTab(statuses[index]);
|
||||
},
|
||||
tabs: const [
|
||||
Tab(text: "جارية", icon: Icon(Icons.directions_car)),
|
||||
Tab(text: "جديدة", icon: Icon(Icons.new_releases)),
|
||||
Tab(text: "مكتملة", icon: Icon(Icons.check_circle)),
|
||||
Tab(text: "ملغاة", icon: Icon(Icons.cancel)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
onChanged: (val) => controller.filterRides(val),
|
||||
decoration: InputDecoration(
|
||||
hintText: "بحث...",
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value)
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
if (controller.displayedRides.isEmpty)
|
||||
return const Center(child: Text("لا توجد رحلات"));
|
||||
|
||||
return ListView.builder(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
itemCount: controller.displayedRides.length,
|
||||
itemBuilder: (context, index) {
|
||||
final ride = controller.displayedRides[index];
|
||||
return _buildRideCard(
|
||||
ride, controller.isSuperAdmin); // نمرر صلاحية الأدمن
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideCard(RideDashboardModel ride, bool isAdmin) {
|
||||
Color statusColor = _getStatusColor(ride.status);
|
||||
String statusText = _getStatusText(ride.status);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => Get.to(() => RideMapMonitorScreen(
|
||||
ride: ride, isAdmin: isAdmin)), // نمرر الصلاحية للخريطة أيضاً
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("رحلة #${ride.rideId}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: statusColor)),
|
||||
child: Text(statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Stats
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade200)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_statItem(
|
||||
Icons.attach_money,
|
||||
"السعر",
|
||||
"${double.tryParse(ride.price)?.toStringAsFixed(0) ?? 0}",
|
||||
Colors.green),
|
||||
Container(
|
||||
width: 1, height: 25, color: Colors.grey.shade300),
|
||||
_statItem(
|
||||
Icons.social_distance,
|
||||
"المسافة",
|
||||
"${double.tryParse(ride.distance)?.toStringAsFixed(1) ?? 0} كم",
|
||||
Colors.blue),
|
||||
Container(
|
||||
width: 1, height: 25, color: Colors.grey.shade300),
|
||||
_statItem(
|
||||
Icons.access_time,
|
||||
"الوقت",
|
||||
ride.time.length > 5
|
||||
? ride.time.substring(0, 5)
|
||||
: ride.time,
|
||||
Colors.orange),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_locationRow(Icons.my_location, ride.startLocation, Colors.blue),
|
||||
const SizedBox(height: 6),
|
||||
_locationRow(Icons.location_on, ride.endLocation, Colors.red),
|
||||
const Divider(height: 20),
|
||||
|
||||
// === معلومات السائق والراكب مع ميزة إخفاء الرقم ===
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _userInfo(
|
||||
title: "الكابتن",
|
||||
name: ride.driverName,
|
||||
phone: ride.driverPhone,
|
||||
isAdmin: isAdmin,
|
||||
completed: ride.driverCompletedCount,
|
||||
canceled: ride.driverCanceledCount)),
|
||||
Container(width: 1, height: 40, color: Colors.grey.shade300),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: _userInfo(
|
||||
title: "الراكب",
|
||||
name: ride.passengerName,
|
||||
phone: ride.passengerPhone,
|
||||
isAdmin: isAdmin,
|
||||
completed: ride.passengerCompletedCount)),
|
||||
],
|
||||
),
|
||||
|
||||
if ((ride.status.contains('Cancel') ||
|
||||
ride.status == 'TimeOut') &&
|
||||
ride.cancelReason.isNotEmpty &&
|
||||
ride.cancelReason != 'لا يوجد سبب') ...[
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.shade200)),
|
||||
child: Text("السبب: ${ride.cancelReason}",
|
||||
style:
|
||||
TextStyle(color: Colors.red.shade900, fontSize: 13)),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === ويدجت عرض المعلومات مع منطق الإخفاء ===
|
||||
Widget _userInfo(
|
||||
{required String title,
|
||||
required String name,
|
||||
required String phone,
|
||||
required bool isAdmin,
|
||||
String? completed,
|
||||
String? canceled}) {
|
||||
// 1. منطق الإخفاء (Masking)
|
||||
String displayPhone = phone;
|
||||
if (!isAdmin && phone.length > 4) {
|
||||
// إظهار آخر 4 أرقام فقط
|
||||
displayPhone =
|
||||
phone.substring(phone.length - 4).padLeft(phone.length, '*');
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
Text(name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
|
||||
// 2. رقم الهاتف وزر الاتصال
|
||||
Row(
|
||||
children: [
|
||||
Text(displayPhone,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.grey, letterSpacing: 1)),
|
||||
if (isAdmin && phone.isNotEmpty) ...[
|
||||
const SizedBox(width: 4),
|
||||
InkWell(
|
||||
onTap: () => _makePhoneCall(phone),
|
||||
child: const Icon(Icons.call, size: 14, color: Colors.green),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
|
||||
if (completed != null)
|
||||
Text("تم: $completed ${canceled != null ? '| ألغى: $canceled' : ''}",
|
||||
style: const TextStyle(fontSize: 9, color: Colors.black54)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _makePhoneCall(String phoneNumber) async {
|
||||
// التحقق مما إذا كانت العلامة موجودة مسبقاً لتجنب التكرار (++963)
|
||||
String formattedPhone = phoneNumber;
|
||||
if (!formattedPhone.startsWith('+')) {
|
||||
formattedPhone = '+$formattedPhone';
|
||||
}
|
||||
|
||||
final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone);
|
||||
|
||||
if (await canLaunchUrl(launchUri)) {
|
||||
await launchUrl(launchUri);
|
||||
} else {
|
||||
// يمكنك هنا إضافة تنبيه بسيط في حال فشل فتح التطبيق
|
||||
debugPrint("لا يمكن الاتصال بالرقم: $formattedPhone");
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
Color _getStatusColor(String s) {
|
||||
if (s == 'Begin' || s == 'Arrived') return Colors.green;
|
||||
if (s == 'Finished') return Colors.teal;
|
||||
if (s.contains('Cancel') || s == 'TimeOut') return Colors.red;
|
||||
if (s == 'New') return Colors.blue;
|
||||
return Colors.grey;
|
||||
}
|
||||
|
||||
String _getStatusText(String s) {
|
||||
if (s == 'Begin' || s == 'Arrived') return "جارية 🟢";
|
||||
if (s == 'Finished') return "مكتملة ✅";
|
||||
if (s == 'CancelFromDriver' || s == 'CancelFromDriverAfterApply')
|
||||
return "ألغاها السائق 👨✈️";
|
||||
if (s == 'CancelFromPassenger') return "ألغاها الراكب 👤";
|
||||
if (s == 'TimeOut') return "انتهى الوقت ⏱️";
|
||||
if (s == 'New') return "جديدة 🆕";
|
||||
return "ملغاة ❌";
|
||||
}
|
||||
|
||||
Widget _statItem(IconData icon, String label, String value, Color color) {
|
||||
return Column(children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
Text(value,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey))
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _locationRow(IconData icon, String text, Color color) {
|
||||
return Row(children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13)))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 4. MAP MONITOR SCREEN
|
||||
// ==========================================
|
||||
class RideMapMonitorScreen extends StatefulWidget {
|
||||
final RideDashboardModel ride;
|
||||
final bool isAdmin; // نستقبل الصلاحية هنا أيضاً
|
||||
const RideMapMonitorScreen(
|
||||
{super.key, required this.ride, required this.isAdmin});
|
||||
|
||||
@override
|
||||
State<RideMapMonitorScreen> createState() => _RideMapMonitorScreenState();
|
||||
}
|
||||
|
||||
class _RideMapMonitorScreenState extends State<RideMapMonitorScreen> {
|
||||
final MapController mapController = MapController();
|
||||
LatLng? startPos, endPos, driverPos;
|
||||
Timer? _timer;
|
||||
bool isFirstLoad = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
startPos = widget.ride.getStartLatLng();
|
||||
endPos = widget.ride.getEndLatLng();
|
||||
if (widget.ride.status == 'Begin' || widget.ride.status == 'Arrived') {
|
||||
fetchDriverLocation();
|
||||
_timer = Timer.periodic(
|
||||
const Duration(seconds: 10), (_) => fetchDriverLocation());
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _fitBounds());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
mapController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _fitBounds() {
|
||||
List<LatLng> points = [];
|
||||
if (startPos != null) points.add(startPos!);
|
||||
if (endPos != null) points.add(endPos!);
|
||||
if (driverPos != null) points.add(driverPos!);
|
||||
if (points.isNotEmpty) {
|
||||
try {
|
||||
mapController.fitCamera(CameraFit.bounds(
|
||||
bounds: LatLngBounds.fromPoints(points),
|
||||
padding: const EdgeInsets.all(50)));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchDriverLocation() async {
|
||||
String trackUrl =
|
||||
"https://api.intaleq.xyz/intaleq/Admin/rides/get_driver_live_pos.php";
|
||||
try {
|
||||
var response = await CRUD()
|
||||
.post(link: trackUrl, payload: {"driver_id": widget.ride.driverId});
|
||||
if (response != 'failure') {
|
||||
var d = response['message'];
|
||||
setState(() {
|
||||
driverPos = LatLng(double.parse(d['latitude'].toString()),
|
||||
double.parse(d['longitude'].toString()));
|
||||
});
|
||||
if (isFirstLoad) {
|
||||
_fitBounds();
|
||||
isFirstLoad = false;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("تتبع الرحلة #${widget.ride.rideId}"),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 1),
|
||||
body: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: startPos ?? const LatLng(33.513, 36.276),
|
||||
initialZoom: 13),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png'),
|
||||
if (startPos != null && endPos != null)
|
||||
PolylineLayer(polylines: [
|
||||
Polyline(
|
||||
points: [startPos!, endPos!],
|
||||
strokeWidth: 4,
|
||||
color: Colors.blue.withOpacity(0.7))
|
||||
]),
|
||||
MarkerLayer(markers: [
|
||||
if (startPos != null)
|
||||
Marker(
|
||||
point: startPos!,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child:
|
||||
const Icon(Icons.flag, color: Colors.green, size: 40),
|
||||
alignment: Alignment.topCenter),
|
||||
if (endPos != null)
|
||||
Marker(
|
||||
point: endPos!,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: const Icon(Icons.location_on,
|
||||
color: Colors.red, size: 40),
|
||||
alignment: Alignment.topCenter),
|
||||
if (driverPos != null)
|
||||
Marker(
|
||||
point: driverPos!,
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(blurRadius: 5, color: Colors.black26)
|
||||
]),
|
||||
child: const Icon(Icons.directions_car,
|
||||
color: Colors.blue, size: 30))),
|
||||
]),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
left: 15,
|
||||
right: 15,
|
||||
child: Card(
|
||||
elevation: 5,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("السعر: ${widget.ride.price}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green)),
|
||||
Text("المسافة: ${widget.ride.distance} كم")
|
||||
]),
|
||||
const Divider(),
|
||||
_mapInfo(Icons.person, "الكابتن: ${widget.ride.driverName}",
|
||||
widget.ride.driverPhone),
|
||||
const SizedBox(height: 5),
|
||||
_mapInfo(
|
||||
Icons.person_outline,
|
||||
"الراكب: ${widget.ride.passengerName}",
|
||||
widget.ride.passengerPhone),
|
||||
const SizedBox(height: 5),
|
||||
_simpleInfo(
|
||||
Icons.my_location, "من: ${widget.ride.startLocation}"),
|
||||
_simpleInfo(
|
||||
Icons.location_on, "إلى: ${widget.ride.endLocation}"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
mini: true,
|
||||
child: const Icon(Icons.center_focus_strong),
|
||||
onPressed: _fitBounds),
|
||||
);
|
||||
}
|
||||
|
||||
// ويدجت خاصة بالخريطة تطبق نفس منطق الإخفاء
|
||||
Widget _mapInfo(IconData icon, String text, String phone) {
|
||||
String displayPhone = phone;
|
||||
if (!widget.isAdmin && phone.length > 4) {
|
||||
displayPhone =
|
||||
phone.substring(phone.length - 4).padLeft(phone.length, '*');
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Colors.grey[700]),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text("$text ($displayPhone)",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14))),
|
||||
if (widget.isAdmin && phone.isNotEmpty)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final Uri launchUri = Uri(scheme: 'tel', path: phone);
|
||||
if (await canLaunchUrl(launchUri)) await launchUrl(launchUri);
|
||||
},
|
||||
child: const Icon(Icons.call, size: 18, color: Colors.green),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _simpleInfo(IconData icon, String text) {
|
||||
return Row(children: [
|
||||
Icon(icon, size: 18, color: Colors.grey[700]),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14)))
|
||||
]);
|
||||
}
|
||||
}
|
||||
163
lib/views/admin/static/notes_driver_page.dart
Normal file
163
lib/views/admin/static/notes_driver_page.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:sefer_admin1/controller/functions/launch.dart';
|
||||
|
||||
import '../../../controller/admin/static_controller.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
|
||||
class DailyNotesView extends StatelessWidget {
|
||||
const DailyNotesView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// نستخدم نفس الكونترولر للوصول لدالة جلب الملاحظات
|
||||
final controller = Get.find<StaticController>();
|
||||
|
||||
// عند فتح الصفحة، نجلب ملاحظات اليوم الحالي
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchDailyNotes(DateTime.now());
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F2F5),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'سجل المكالمات اليومي',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF1A1A1A),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
iconTheme: const IconThemeData(color: Colors.black87),
|
||||
),
|
||||
body: GetBuilder<StaticController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoadingNotes) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.dailyNotesList.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.note_alt_outlined,
|
||||
size: 80, color: Colors.grey.shade300),
|
||||
const SizedBox(height: 10),
|
||||
Text("لا توجد سجلات لهذا اليوم",
|
||||
style: TextStyle(color: Colors.grey.shade600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: controller.dailyNotesList.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final note = controller.dailyNotesList[index];
|
||||
final String name = note['editor'] ?? note['name'] ?? 'Unknown';
|
||||
final String phone = note['phone'] ?? note['phone'] ?? 'Unknown';
|
||||
final String content = note['note'] ?? note['content'] ?? '';
|
||||
final String time = note['createdAt'] ?? '';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: _getEmployeeColor(name), width: 4))),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor:
|
||||
_getEmployeeColor(name).withOpacity(0.1),
|
||||
child: Icon(Icons.person,
|
||||
size: 16, color: _getEmployeeColor(name)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
name.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
fontSize: 14),
|
||||
),
|
||||
const SizedBox(width: 100),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
makePhoneCall('+$phone');
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
phone,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
fontSize: 14),
|
||||
),
|
||||
Icon(Icons.phone)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 22),
|
||||
Text(
|
||||
time.split(' ').last, // عرض الوقت فقط
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade400, fontSize: 12),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 20),
|
||||
Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade700,
|
||||
height: 1.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getEmployeeColor(String name) {
|
||||
String n = name.toLowerCase().trim();
|
||||
if (n.contains('shahd')) return Colors.redAccent;
|
||||
if (n.contains('mayar')) return Colors.amber.shade700;
|
||||
if (n.contains('rama2')) return Colors.green;
|
||||
if (n.contains('rama1')) return Colors.blue;
|
||||
return Colors.blueGrey;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ import 'package:sefer_admin1/env/env.dart';
|
||||
|
||||
import '../../controller/auth/login_controller.dart';
|
||||
import '../../controller/auth/otp_helper.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../../print.dart';
|
||||
|
||||
class AdminLoginPage extends StatefulWidget {
|
||||
const AdminLoginPage({super.key});
|
||||
@@ -17,6 +19,7 @@ class _AdminLoginPageState extends State<AdminLoginPage> {
|
||||
bool _isLoading = false;
|
||||
Future<void> _submit() async {
|
||||
final allowedPhones = Env.ALLOWED_ADMIN_PHONES;
|
||||
Log.print('allowedPhones: ${allowedPhones}');
|
||||
allowedPhones.toString().split(',');
|
||||
|
||||
final phone = _phoneController.text.trim();
|
||||
@@ -36,6 +39,16 @@ class _AdminLoginPageState extends State<AdminLoginPage> {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeToken(); // استدعاء دالة async بدون await
|
||||
}
|
||||
|
||||
void _initializeToken() async {
|
||||
await CRUD().getJWT();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(OtpHelper());
|
||||
|
||||
@@ -39,6 +39,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
||||
final driverID = '123'; // ← عدّله حسب نظامك
|
||||
final invoiceNumber = generateInvoiceNumber();
|
||||
final amount = _amountController.text.trim();
|
||||
final itemName = _itemNameController.text.trim();
|
||||
final date = DateTime.now().toIso8601String().split('T').first;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
@@ -54,6 +55,7 @@ class _AddInvoicePageState extends State<AddInvoicePage> {
|
||||
..fields['driverID'] = driverID
|
||||
..fields['invoiceNumber'] = invoiceNumber
|
||||
..fields['amount'] = amount
|
||||
..fields['name'] = itemName
|
||||
..fields['date'] = date
|
||||
..headers.addAll(headers);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class MyTextForm extends StatelessWidget {
|
||||
return 'Please enter a valid email.'.tr;
|
||||
}
|
||||
} else if (type == TextInputType.phone) {
|
||||
if (value.length != 11) {
|
||||
if (value.length != 10) {
|
||||
return 'Please enter a valid phone number.'.tr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user