Update: 2026-06-14 22:10:07
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_service/constant/links.dart';
|
||||
import 'package:siro_service/controller/functions/crud.dart';
|
||||
import 'package:siro_service/views/widgets/my_scafold.dart';
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// نموذج بيانات الرحلة
|
||||
/// --------------------------------------------------------------------------
|
||||
class ActiveRideModel {
|
||||
final String rideId;
|
||||
final String status;
|
||||
final String startLocation;
|
||||
final String endLocation;
|
||||
final String driverName;
|
||||
final String driverPhone;
|
||||
final String passengerName;
|
||||
final String passengerPhone;
|
||||
final String price;
|
||||
final String carType;
|
||||
final String date;
|
||||
|
||||
ActiveRideModel({
|
||||
required this.rideId,
|
||||
required this.status,
|
||||
required this.startLocation,
|
||||
required this.endLocation,
|
||||
required this.driverName,
|
||||
required this.driverPhone,
|
||||
required this.passengerName,
|
||||
required this.passengerPhone,
|
||||
required this.price,
|
||||
required this.carType,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
factory ActiveRideModel.fromJson(Map<String, dynamic> json) {
|
||||
final ride = json['ride_details'] ?? json;
|
||||
final driver = json['driver_details'] ?? {};
|
||||
return ActiveRideModel(
|
||||
rideId: ride['id'].toString(),
|
||||
status: ride['status'] ?? '',
|
||||
startLocation: ride['start_location'] ?? '',
|
||||
endLocation: ride['end_location'] ?? '',
|
||||
driverName: driver['fullname'] ?? driver['first_name'] ?? 'غير معروف',
|
||||
driverPhone: driver['phone'] ?? '',
|
||||
passengerName: '',
|
||||
passengerPhone: '',
|
||||
price: ride['price']?.toString() ?? '0',
|
||||
carType: ride['carType'] ?? '',
|
||||
date: ride['date'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// تطبيع رقم الهاتف تلقائياً حسب الدولة
|
||||
/// --------------------------------------------------------------------------
|
||||
String normalizePhone(String input) {
|
||||
final clean = input.replaceAll(RegExp(r'\D+'), '');
|
||||
// Syria
|
||||
if (clean.length == 10 && clean.startsWith('09'))
|
||||
return '963${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('963')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('9')) return '963$clean';
|
||||
// Jordan
|
||||
if (clean.length == 10 && clean.startsWith('07'))
|
||||
return '962${clean.substring(1)}';
|
||||
if (clean.length == 12 && clean.startsWith('962')) return clean;
|
||||
if (clean.length == 9 && clean.startsWith('7')) return '962$clean';
|
||||
// Egypt
|
||||
if (clean.length == 11 && clean.startsWith('01'))
|
||||
return '20${clean.substring(1)}';
|
||||
if (clean.length == 13 && clean.startsWith('20')) return clean;
|
||||
return clean;
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// الـ Controller
|
||||
/// --------------------------------------------------------------------------
|
||||
class RideMonitorServiceController extends GetxController {
|
||||
final TextEditingController phoneCtrl = TextEditingController();
|
||||
final TextEditingController rideIdCtrl = TextEditingController();
|
||||
|
||||
var isLoading = false.obs;
|
||||
var hasResult = false.obs;
|
||||
var hasError = false.obs;
|
||||
var errorMessage = ''.obs;
|
||||
var activeRides = <ActiveRideModel>[].obs;
|
||||
var searchMode = 'phone'.obs; // 'phone' or 'ride_id'
|
||||
|
||||
final String monitorApiUrl = "${AppLink.server}/Admin/rides/monitorRide.php";
|
||||
final String searchApiUrl =
|
||||
"${AppLink.server}/Admin/rides/admin_get_rides_by_phone.php";
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
phoneCtrl.dispose();
|
||||
rideIdCtrl.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// البحث برقم هاتف الراكب أو السائق
|
||||
Future<void> searchByPhone() async {
|
||||
final phone = phoneCtrl.text.trim();
|
||||
if (phone.isEmpty) {
|
||||
Get.snackbar('تنبيه', 'يرجى إدخال رقم الهاتف',
|
||||
backgroundColor: Colors.redAccent, colorText: Colors.white);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
hasResult.value = false;
|
||||
activeRides.clear();
|
||||
|
||||
try {
|
||||
final normalizedPhone = normalizePhone(phone);
|
||||
|
||||
// محاولة البحث أولاً عن رحلة نشطة
|
||||
var res = await CRUD().post(
|
||||
link: monitorApiUrl,
|
||||
payload: {'phone': normalizedPhone},
|
||||
);
|
||||
|
||||
if (res != 'failure' && res is Map && res['status'] == 'success') {
|
||||
var data = (res['message'] ?? res['data'] ?? res) as Map;
|
||||
if (data['ride_details'] != null) {
|
||||
activeRides
|
||||
.add(ActiveRideModel.fromJson(Map<String, dynamic>.from(data)));
|
||||
hasResult.value = true;
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// إذا لم نجد رحلة نشطة، نبحث عن آخر الرحلات
|
||||
var res2 = await CRUD().post(
|
||||
link: searchApiUrl,
|
||||
payload: {'phone': normalizedPhone},
|
||||
);
|
||||
|
||||
if (res2 != 'failure' && res2 is Map && res2['status'] == 'success') {
|
||||
var data = (res2['message'] ?? res2['data'] ?? res2) as Map;
|
||||
final rides = data['rides'] as List?;
|
||||
if (rides != null && rides.isNotEmpty) {
|
||||
for (var ride in rides) {
|
||||
final rideMap = ride as Map;
|
||||
activeRides.add(ActiveRideModel.fromJson({
|
||||
'ride_details': ride,
|
||||
'driver_details': {
|
||||
'fullname': rideMap['driver_first_name'] ?? '',
|
||||
'phone': rideMap['d_phone'] ?? '',
|
||||
}
|
||||
}));
|
||||
}
|
||||
hasResult.value = true;
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'لا توجد رحلات لهذا الرقم';
|
||||
}
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'لم يتم العثور على المستخدم';
|
||||
}
|
||||
} catch (e) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'خطأ في الاتصال: $e';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// البحث برقم الرحلة (Ride ID)
|
||||
Future<void> searchByRideId() async {
|
||||
final rideId = rideIdCtrl.text.trim();
|
||||
if (rideId.isEmpty) {
|
||||
Get.snackbar('تنبيه', 'يرجى إدخال رقم الرحلة',
|
||||
backgroundColor: Colors.redAccent, colorText: Colors.white);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
hasResult.value = false;
|
||||
activeRides.clear();
|
||||
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link: "${AppLink.server}/Admin/rides/get_rides_by_status.php",
|
||||
payload: {'status': 'All'},
|
||||
);
|
||||
|
||||
if (res != 'failure' && res is Map && res['status'] == 'success') {
|
||||
final rawRides = res['message'];
|
||||
final rides = (rawRides is List ? rawRides : []) as List;
|
||||
final found = rides.where((r) {
|
||||
final rm = r as Map;
|
||||
return rm['id'].toString() == rideId;
|
||||
}).toList();
|
||||
|
||||
if (found.isNotEmpty) {
|
||||
for (var ride in found) {
|
||||
final rideMap = ride as Map;
|
||||
activeRides.add(ActiveRideModel.fromJson({
|
||||
'ride_details': ride,
|
||||
'driver_details': {
|
||||
'fullname': rideMap['driver_full_name'] ?? '',
|
||||
'phone': rideMap['d_phone'] ?? '',
|
||||
}
|
||||
}));
|
||||
}
|
||||
hasResult.value = true;
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'لم يتم العثور على رحلة بهذا الرقم';
|
||||
}
|
||||
} else {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'فشل في جلب الرحلات';
|
||||
}
|
||||
} catch (e) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = 'خطأ في الاتصال: $e';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// الشاشة الرئيسية
|
||||
/// --------------------------------------------------------------------------
|
||||
class RideMonitorServicePage extends StatelessWidget {
|
||||
const RideMonitorServicePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(RideMonitorServiceController());
|
||||
|
||||
return MyScaffold(
|
||||
title: 'متابعة الرحلات',
|
||||
isleading: true,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🎯 اختيار طريقة البحث
|
||||
Obx(() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildModeButton(
|
||||
'رقم الهاتف',
|
||||
Icons.phone_rounded,
|
||||
controller.searchMode.value == 'phone',
|
||||
() => controller.searchMode.value = 'phone',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildModeButton(
|
||||
'رقم الرحلة',
|
||||
Icons.confirmation_number_rounded,
|
||||
controller.searchMode.value == 'ride_id',
|
||||
() => controller.searchMode.value = 'ride_id',
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 🔍 حقل البحث
|
||||
Obx(() => controller.searchMode.value == 'phone'
|
||||
? _buildPhoneSearch(controller)
|
||||
: _buildRideIdSearch(controller)),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 📋 النتائج
|
||||
Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.hasError.value) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.error_outline_rounded,
|
||||
color: Colors.red, size: 40),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
controller.errorMessage.value,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.hasResult.value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'نتائج البحث (${controller.activeRides.length})',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...controller.activeRides.map(
|
||||
(ride) => _buildRideCard(ride),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeButton(
|
||||
String title, IconData icon, bool isActive, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? const Color(0xFF4318FF) : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon,
|
||||
color: isActive ? Colors.white : Colors.grey.shade600,
|
||||
size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isActive ? Colors.white : Colors.grey.shade600,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneSearch(RideMonitorServiceController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.phoneCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'رقم الهاتف',
|
||||
hintText: 'مثال: 0992952235 أو 079XXXXXXX',
|
||||
prefixIcon: const Icon(Icons.phone_rounded),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => controller.searchByPhone(),
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
label: const Text('بحث عن رحلات',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF4318FF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideIdSearch(RideMonitorServiceController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.rideIdCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'رقم الرحلة',
|
||||
hintText: 'مثال: 12345',
|
||||
prefixIcon: const Icon(Icons.confirmation_number_rounded),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => controller.searchByRideId(),
|
||||
icon: const Icon(Icons.search_rounded),
|
||||
label: const Text('بحث برقم الرحلة',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF4318FF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRideCard(ActiveRideModel ride) {
|
||||
Color statusColor;
|
||||
switch (ride.status) {
|
||||
case 'Begin':
|
||||
statusColor = const Color(0xFF10B981);
|
||||
break;
|
||||
case 'Apply':
|
||||
case 'Applied':
|
||||
statusColor = const Color(0xFF3B82F6);
|
||||
break;
|
||||
case 'Finished':
|
||||
statusColor = const Color(0xFF14B8A6);
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'رحلة #${ride.rideId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
ride.status,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(ride.date,
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||
const Divider(height: 20),
|
||||
// Info rows
|
||||
_buildInfoRow(Icons.person_rounded, 'السائق: ${ride.driverName}'),
|
||||
if (ride.driverPhone.isNotEmpty)
|
||||
_buildInfoRow(
|
||||
Icons.phone_rounded, 'هاتف السائق: ${ride.driverPhone}'),
|
||||
_buildInfoRow(Icons.payments_rounded, 'السعر: ${ride.price}'),
|
||||
if (ride.carType.isNotEmpty)
|
||||
_buildInfoRow(
|
||||
Icons.directions_car_rounded, 'نوع السيارة: ${ride.carType}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(IconData icon, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(text, style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import 'package:siro_service/views/widgets/my_dialog.dart';
|
||||
import 'package:siro_service/views/widgets/my_textField.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/mainController/pages/ride_monitor_page.dart';
|
||||
import '../../controller/mainController/pages/add_car.dart';
|
||||
import '../../controller/mainController/pages/drivers_cant_register.dart';
|
||||
import '../../controller/mainController/pages/new_driver.dart';
|
||||
@@ -174,6 +175,16 @@ class Main extends StatelessWidget {
|
||||
),
|
||||
]),
|
||||
|
||||
_buildCategoryTitle('🚗 متابعة الرحلات'.tr),
|
||||
_buildGridSection([
|
||||
ServiceItem(
|
||||
title: 'متابعة رحلة',
|
||||
icon: Icons.map_rounded,
|
||||
color: const Color(0xFF4318FF),
|
||||
onTap: () => Get.to(() => const RideMonitorServicePage()),
|
||||
),
|
||||
]),
|
||||
|
||||
_buildCategoryTitle('📊 Reporting & Quality'.tr),
|
||||
_buildGridSection([
|
||||
ServiceItem(
|
||||
|
||||
Reference in New Issue
Block a user