Files
intaleq_admin/lib/views/admin/rides/ride_lookup_page.dart
2026-03-10 00:02:17 +03:00

1214 lines
43 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:sefer_admin1/constant/links.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../controller/functions/crud.dart';
import '../../../constant/box_name.dart';
import '../../../main.dart';
// ═══════════════════════════════════════════════════════════════════════════
// 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 = "${AppLink.server}/Admin/rides/get_rides_by_status.php";
// ═══ Statistics ═══
var beginCount = 0.obs;
var newCount = 0.obs;
var completedCount = 0.obs;
var canceledCount = 0.obs;
var totalRevenue = 0.0.obs;
var totalDistance = 0.0.obs;
@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();
}
}
void calculateStatistics() {
beginCount.value = allRidesList.where((r) => r.status == 'Begin').length;
newCount.value = allRidesList.where((r) => r.status == 'New').length;
completedCount.value =
allRidesList.where((r) => r.status == 'Finished').length;
canceledCount.value =
allRidesList.where((r) => r.status.contains('Cancel')).length;
totalRevenue.value = allRidesList.fold(
0.0, (sum, ride) => sum + (double.tryParse(ride.price) ?? 0.0));
totalDistance.value = allRidesList.fold(
0.0, (sum, ride) => sum + (double.tryParse(ride.distance) ?? 0.0));
}
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;
calculateStatistics();
}
} catch (e) {
debugPrint("Error fetching rides: $e");
} finally {
isLoading.value = false;
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 3. MAIN DASHBOARD SCREEN (ADVANCED SLIVER IMPLEMENTATION)
// ═══════════════════════════════════════════════════════════════════════════
class RidesDashboardScreen extends StatefulWidget {
const RidesDashboardScreen({super.key});
@override
State<RidesDashboardScreen> createState() => _RidesDashboardScreenState();
}
class _RidesDashboardScreenState extends State<RidesDashboardScreen>
with SingleTickerProviderStateMixin {
late final RidesListController controller;
late TabController _tabController;
// 🎨 الألوان العصرية
final Color bgColor = const Color(0xFFF4F7FE);
final Color primaryColor = const Color(0xFF4318FF);
final Color textPrimary = const Color(0xFF2B3674);
@override
void initState() {
super.initState();
controller = Get.put(RidesListController());
_tabController = TabController(length: 4, vsync: this);
_tabController.addListener(_handleTabChange);
}
@override
void dispose() {
_tabController.removeListener(_handleTabChange);
_tabController.dispose();
super.dispose();
}
void _handleTabChange() {
if (_tabController.indexIsChanging) return;
List<String> statuses = ['Begin', 'New', 'Completed', 'Canceled'];
controller.changeTab(statuses[_tabController.index]);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: bgColor,
body: SafeArea(
top: false, // نسمح للـ AppBar بالتمدد لأعلى الشاشة
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// 1. Sliver AppBar (المتحرك الذكي الذي يصغر عند التمرير)
SliverAppBar(
pinned: true,
floating: false,
expandedHeight: 310.0, // ارتفاع الجزء العلوي بالكامل
backgroundColor: primaryColor,
elevation: 4,
shadowColor: primaryColor.withOpacity(0.4),
iconTheme: const IconThemeData(color: Colors.white),
centerTitle: true,
title: const Text(
'إدارة الرحلات',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
actions: [
Container(
margin:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.center,
child: const Text(
'اليوم',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primaryColor, primaryColor.withOpacity(0.8)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// الإحصائيات تختفي عند التمرير لأعلى
_buildStatisticsSection(),
const SizedBox(height: 12),
// شريط البحث يختفي عند التمرير لأعلى
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildSearchBar(),
),
const SizedBox(height: 60), // مساحة للـ TabBar بالأسفل
],
),
),
),
// TabBar يثبت دائماً أسفل الـ AppBar عند التمرير
bottom: PreferredSize(
preferredSize: const Size.fromHeight(54),
child: Container(
decoration: const BoxDecoration(
color:
Color(0xFFF4F7FE), // لون خلفية التطبيق ليظهر بشكل مدمج
borderRadius:
BorderRadius.vertical(top: Radius.circular(24)),
),
child: TabBar(
controller: _tabController,
isScrollable: true,
labelColor: primaryColor,
unselectedLabelColor: Colors.grey,
indicatorColor: primaryColor,
indicatorWeight: 3,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14),
tabAlignment: TabAlignment.center,
dividerColor: Colors.transparent,
tabs: const [
Tab(
icon: Icon(Icons.directions_car_rounded),
text: 'جارية'),
Tab(
icon: Icon(Icons.new_releases_rounded),
text: 'جديدة'),
Tab(
icon: Icon(Icons.check_circle_rounded),
text: 'مكتملة'),
Tab(icon: Icon(Icons.cancel_rounded), text: 'ملغاة'),
],
),
),
),
),
// 2. قائمة الرحلات (Sliver List)
SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: Obx(() {
if (controller.isLoading.value) {
return const SliverFillRemaining(
hasScrollBody: false,
child: Center(child: CircularProgressIndicator()),
);
}
if (controller.displayedRides.isEmpty) {
return SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_rounded,
size: 80, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'لا توجد رحلات في هذا القسم',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final ride = controller.displayedRides[index];
return _buildRideCardCompact(
ride, controller.isSuperAdmin);
},
childCount: controller.displayedRides.length,
),
);
}),
),
],
),
),
);
}
// --- Components ---
Widget _buildStatisticsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text(
'نظرة عامة',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white.withOpacity(0.8),
),
),
),
SingleChildScrollView(
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Obx(() => _buildStatCard(
'جارية',
controller.beginCount.toString(),
Icons.directions_car_rounded,
const Color(0xFF10B981))),
Obx(() => _buildStatCard('جديدة', controller.newCount.toString(),
Icons.new_releases_rounded, const Color(0xFF3B82F6))),
Obx(() => _buildStatCard(
'مكتملة',
controller.completedCount.toString(),
Icons.check_circle_rounded,
const Color(0xFF14B8A6))),
Obx(() => _buildStatCard(
'ملغاة',
controller.canceledCount.toString(),
Icons.cancel_rounded,
const Color(0xFFEF4444))),
Obx(() => _buildStatCard(
'الإيرادات',
'${controller.totalRevenue.value.toStringAsFixed(0)}',
Icons.payments_rounded,
const Color(0xFFF59E0B))),
],
),
),
],
);
}
Widget _buildStatCard(
String label, String value, IconData icon, Color iconColor) {
return Container(
width: 105,
margin: const EdgeInsets.only(left: 10),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: iconColor, size: 24),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: textPrimary),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
fontWeight: FontWeight.bold),
),
],
),
);
}
Widget _buildSearchBar() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 4),
)
],
),
child: TextField(
controller: controller.searchController,
onChanged: (val) => controller.filterRides(val),
style: TextStyle(color: textPrimary, fontWeight: FontWeight.w600),
decoration: InputDecoration(
hintText: 'ابحث عن رقم الرحلة، السائق، أو الراكب...',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
prefixIcon: Icon(Icons.search_rounded, color: primaryColor),
suffixIcon: controller.searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.close_rounded, color: Colors.grey),
onPressed: () {
controller.searchController.clear();
controller.filterRides('');
},
)
: null,
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
),
),
);
}
// ═══════════════════════════════════════════════════════════════════════════
// تصميم بطاقة الرحلة (المدمج والمنظم - Slim Design)
// ═══════════════════════════════════════════════════════════════════════════
Widget _buildRideCardCompact(RideDashboardModel ride, bool isAdmin) {
Color statusColor = _getStatusColor(ride.status);
String statusText = _getStatusText(ride.status);
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => Get.to(
() => RideMapMonitorScreen(ride: ride, isAdmin: isAdmin),
transition: Transition.cupertino,
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Header (ID + Status)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.confirmation_number_rounded,
color: primaryColor, size: 18),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'رحلة #${ride.rideId}',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: textPrimary),
),
Text(
'${ride.date}${ride.time}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.w600),
),
],
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: statusColor),
),
),
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(height: 1),
),
// 2. Locations (Timeline style)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
const Icon(Icons.my_location_rounded,
size: 14, color: Color(0xFF10B981)),
Container(
width: 2,
height: 16,
color: Colors.grey.withOpacity(0.3),
margin: const EdgeInsets.symmetric(vertical: 2)),
const Icon(Icons.location_on_rounded,
size: 14, color: Color(0xFFEF4444)),
],
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ride.startLocation,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.black87),
),
const SizedBox(height: 12),
Text(
ride.endLocation,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.black87),
),
],
),
),
],
),
const SizedBox(height: 16),
// 3. Driver & Passenger (Slim Rows)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildSlimUserRow(
icon: Icons.local_taxi_rounded,
title: 'السائق',
name: ride.driverName,
phone: ride.driverPhone,
color: const Color(0xFF3B82F6),
isAdmin: isAdmin,
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 6),
child: Divider(height: 1, thickness: 0.5),
),
_buildSlimUserRow(
icon: Icons.person_rounded,
title: 'الراكب',
name: ride.passengerName,
phone: ride.passengerPhone,
color: const Color(0xFF8B5CF6),
isAdmin: isAdmin,
),
],
),
),
const SizedBox(height: 14),
// 4. Information Chips
Row(
children: [
_buildInfoChip(
Icons.payments_rounded,
'${double.tryParse(ride.price)?.toStringAsFixed(0) ?? 0} ل.س',
const Color(0xFF10B981)),
const SizedBox(width: 8),
_buildInfoChip(
Icons.straighten_rounded,
'${double.tryParse(ride.distance)?.toStringAsFixed(1) ?? 0} كم',
const Color(0xFFF59E0B)),
const SizedBox(width: 8),
_buildInfoChip(
Icons.access_time_rounded,
ride.time.length > 5
? ride.time.substring(0, 5)
: ride.time,
const Color(0xFF3B82F6)),
],
),
// 5. Cancel Reason (If any)
if ((ride.status.contains('Cancel') ||
ride.status == 'TimeOut') &&
ride.cancelReason.isNotEmpty &&
ride.cancelReason != 'لا يوجد سبب') ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFFEF4444).withOpacity(0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFFEF4444).withOpacity(0.2)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outline_rounded,
size: 16, color: Color(0xFFEF4444)),
const SizedBox(width: 6),
Expanded(
child: Text(
'السبب: ${ride.cancelReason}',
style: const TextStyle(
color: Color(0xFFB91C1C),
fontSize: 11,
fontWeight: FontWeight.bold),
),
),
],
),
),
],
],
),
),
),
),
);
}
// تصميم الصف النحيف للمستخدم (لتوفير المساحة)
Widget _buildSlimUserRow({
required IconData icon,
required String title,
required String name,
required String phone,
required Color color,
required bool isAdmin,
}) {
String displayPhone = phone;
if (!isAdmin && phone.length > 4) {
displayPhone =
phone.substring(phone.length - 4).padLeft(phone.length, '*');
}
return Row(
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 6),
Text(
'$title:',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
fontWeight: FontWeight.bold),
),
const SizedBox(width: 6),
Expanded(
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
),
Text(
displayPhone,
style: TextStyle(
fontSize: 11, color: Colors.grey[500], letterSpacing: 0.5),
),
if (isAdmin && phone.isNotEmpty) ...[
const SizedBox(width: 8),
GestureDetector(
onTap: () async {
String formattedPhone = phone;
if (!formattedPhone.startsWith('+'))
formattedPhone = '+$formattedPhone';
final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone);
if (await canLaunchUrl(launchUri)) await launchUrl(launchUri);
},
child: Icon(Icons.call_rounded, size: 16, color: color),
),
]
],
);
}
// تصميم الرقاقة (Chip) للمعلومات السفلية
Widget _buildInfoChip(IconData icon, String text, Color color) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 4),
Flexible(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11, fontWeight: FontWeight.bold, color: color),
),
),
],
),
),
);
}
// Helper Methods for Status
Color _getStatusColor(String status) {
if (status == 'Begin' || status == 'Arrived')
return const Color(0xFF10B981);
if (status == 'Finished') return const Color(0xFF14B8A6);
if (status.contains('Cancel') || status == 'TimeOut')
return const Color(0xFFEF4444);
if (status == 'New') return const Color(0xFF3B82F6);
return Colors.grey;
}
String _getStatusText(String status) {
if (status == 'Begin' || status == 'Arrived') return 'جارية';
if (status == 'Finished') return 'مكتملة';
if (status == 'CancelFromDriver' || status == 'CancelFromDriverAfterApply')
return 'ألغى السائق';
if (status == 'CancelFromPassenger') return 'ألغى الراكب';
if (status == 'TimeOut') return 'انتهى الوقت';
if (status == 'New') return 'جديدة';
return 'ملغاة';
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 5. MAP MONITOR SCREEN (Minor UI Polish)
// ═══════════════════════════════════════════════════════════════════════════
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(100),
),
);
} catch (e) {}
}
}
Future<void> fetchDriverLocation() async {
String trackUrl = "${AppLink.server}/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}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF2B3674),
elevation: 0,
centerTitle: true,
),
body: Stack(
children: [
// Map
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',
userAgentPackageName: 'com.tripz.app',
),
// Route Polyline
if (startPos != null && endPos != null)
PolylineLayer(
polylines: [
Polyline(
points: [startPos!, endPos!],
strokeWidth: 5,
color: const Color(0xFF4318FF).withOpacity(0.8),
),
],
),
// Markers
MarkerLayer(
markers: [
// Start Point
if (startPos != null)
Marker(
point: startPos!,
width: 40,
height: 40,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 5),
],
),
child: const Icon(Icons.flag_rounded,
color: Color(0xFF10B981), size: 24),
),
alignment: Alignment.topCenter,
),
// End Point
if (endPos != null)
Marker(
point: endPos!,
width: 40,
height: 40,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 5),
],
),
child: const Icon(Icons.location_on_rounded,
color: Color(0xFFEF4444), size: 24),
),
alignment: Alignment.topCenter,
),
// Driver Current Position
if (driverPos != null)
Marker(
point: driverPos!,
width: 50,
height: 50,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF3B82F6).withOpacity(0.3),
blurRadius: 8),
],
),
child: const Icon(Icons.directions_car_rounded,
color: Color(0xFF3B82F6), size: 28),
),
),
],
),
],
),
// Info Panel (Floating)
Positioned(
bottom: 24,
left: 16,
right: 16,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Driver Info
_buildMapUserInfo(
icon: Icons.local_taxi_rounded,
title: 'السائق',
name: widget.ride.driverName,
phone: widget.ride.driverPhone,
color: const Color(0xFF3B82F6),
),
const SizedBox(height: 12),
// Passenger Info
_buildMapUserInfo(
icon: Icons.person_rounded,
title: 'الراكب',
name: widget.ride.passengerName,
phone: widget.ride.passengerPhone,
color: const Color(0xFF8B5CF6),
),
],
),
),
),
// Fit Button
Positioned(
top: 16,
right: 16,
child: FloatingActionButton.small(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF2B3674),
onPressed: _fitBounds,
child: const Icon(Icons.center_focus_strong_rounded),
),
),
],
),
);
}
Widget _buildMapUserInfo({
required IconData icon,
required String title,
required String name,
required String phone,
required Color color,
}) {
String displayPhone = phone;
if (!widget.isAdmin && phone.length > 4) {
displayPhone =
phone.substring(phone.length - 4).padLeft(phone.length, '*');
}
return Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, size: 20, color: color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
fontWeight: FontWeight.bold),
),
const SizedBox(height: 2),
Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFF2B3674)),
),
],
),
),
if (widget.isAdmin && phone.isNotEmpty)
GestureDetector(
onTap: () async {
String formattedPhone = phone;
if (!formattedPhone.startsWith('+'))
formattedPhone = '+$formattedPhone';
final Uri launchUri = Uri(scheme: 'tel', path: formattedPhone);
if (await canLaunchUrl(launchUri)) await launchUrl(launchUri);
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
shape: BoxShape.circle,
),
child:
const Icon(Icons.call_rounded, size: 20, color: Colors.green),
),
),
],
);
}
}