Files
intaleq_admin/lib/views/admin/rides/ride_lookup_page.dart
2026-01-20 23:39:59 +03:00

698 lines
24 KiB
Dart

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