Update: 2026-06-14 22:10:07

This commit is contained in:
Hamza-Ayed
2026-06-14 22:10:08 +03:00
parent 8e3b9eca4d
commit f021ba5a35
21 changed files with 3669 additions and 636 deletions

View File

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

View File

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