Files
Siro/siro_admin/lib/views/admin/rides/ride_lookup_page.dart
T

1212 lines
43 KiB
Dart

import 'package:siro_admin/constant/map_tiles.dart';
import 'package:siro_admin/constant/theme.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:siro_admin/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;
@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) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
body: SafeArea(
top: false,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// 1. Sliver AppBar (المتحرك الذكي الذي يصغر عند التمرير)
// الرأس: سطح هادئ بدل الكتلة الملوّنة المشبعة.
// النسخة السابقة كانت شريحة بلون primary بارتفاع 310 مع تدرّج
// وظل — تسحب الانتباه إلى الإطار بدل الأرقام، وهي أول ما يجعل
// اللوحة تبدو غير احترافية. اللون الآن يميّز الحالة داخل البطاقات
// فقط، حيث يحمل معنى.
SliverAppBar(
pinned: true,
floating: false,
expandedHeight: 268.0,
backgroundColor: cs.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
iconTheme: IconThemeData(color: cs.onSurface),
centerTitle: false,
titleSpacing: 20,
title: Text(
'إدارة الرحلات',
style: TextStyle(
color: cs.onSurface,
fontWeight: FontWeight.w700,
fontSize: 19,
),
),
actions: [
Container(
margin:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: cs.primary.withValues(alpha: 0.22)),
),
alignment: Alignment.center,
child: Text(
'اليوم',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: cs.primary,
),
),
),
],
flexibleSpace: FlexibleSpaceBar(
background: Container(
color: cs.surface,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildStatisticsSection(cs),
const SizedBox(height: 14),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildSearchBar(cs),
),
const SizedBox(height: 62), // مساحة للـ TabBar بالأسفل
],
),
),
),
// TabBar يثبت دائماً أسفل الـ AppBar عند التمرير
bottom: PreferredSize(
preferredSize: const Size.fromHeight(54),
child: Container(
decoration: BoxDecoration(
color: cs.surface,
border: Border(
bottom: BorderSide(color: cs.outline, width: 1)),
),
child: TabBar(
controller: _tabController,
isScrollable: true,
labelColor: cs.primary,
unselectedLabelColor: cs.onSurfaceVariant,
indicatorColor: cs.primary,
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.label,
labelStyle: const TextStyle(
fontWeight: FontWeight.w700, 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: cs.onSurfaceVariant),
const SizedBox(height: 16),
Text(
'لا توجد رحلات في هذا القسم',
style: TextStyle(
fontSize: 16,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final ride = controller.displayedRides[index];
return _buildRideCardCompact(
ride, controller.isSuperAdmin, cs);
},
childCount: controller.displayedRides.length,
),
);
}),
),
],
),
),
);
}
// --- Components ---
Widget _buildStatisticsSection(ColorScheme cs) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text(
'نظرة عامة',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
// كان Colors.white ثابتاً — يصلح فوق الرأس الملوّن السابق فقط،
// ويصبح غير مقروء إطلاقاً على سطح الوضع الفاتح.
color: cs.onSurfaceVariant,
),
),
),
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,
cs.success, cs)),
Obx(() => _buildStatCard('جديدة', controller.newCount.toString(),
Icons.new_releases_rounded, cs.info, cs)),
Obx(() => _buildStatCard(
'مكتملة',
controller.completedCount.toString(),
Icons.check_circle_rounded,
cs.tertiary, cs)),
Obx(() => _buildStatCard(
'ملغاة',
controller.canceledCount.toString(),
Icons.cancel_rounded,
cs.danger, cs)),
Obx(() => _buildStatCard(
'الإيرادات',
controller.totalRevenue.value.toStringAsFixed(0),
Icons.payments_rounded,
cs.warning, cs)),
],
),
),
],
);
}
Widget _buildStatCard(
String label, String value, IconData icon, Color iconColor, ColorScheme cs) {
// البطاقة تُبنى على سطح مرتفع بحدّ رفيع وشارة لونية للأيقونة، بدل
// بطاقة بيضاء عائمة بظل ثقيل. اللون يعرّف الحالة ولا يملأ المساحة.
return Container(
width: 108,
margin: const EdgeInsets.only(left: 10),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 10),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: iconColor.withValues(alpha: 0.22)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(9),
),
child: Icon(icon, color: iconColor, size: 18),
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: cs.onSurface),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 11,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.bold),
),
],
),
);
}
Widget _buildSearchBar(ColorScheme cs) {
return Container(
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: cs.outline),
),
child: TextField(
controller: controller.searchController,
onChanged: (val) => controller.filterRides(val),
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
decoration: InputDecoration(
hintText: 'ابحث عن رقم الرحلة، السائق، أو الراكب...',
hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
prefixIcon: Icon(Icons.search_rounded, color: cs.primary),
suffixIcon: controller.searchController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.close_rounded, color: cs.onSurfaceVariant),
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, ColorScheme cs) {
Color statusColor = _getStatusColor(ride.status);
String statusText = _getStatusText(ride.status);
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
border: Border.all(color: cs.outline.withValues(alpha: 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: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.confirmation_number_rounded,
color: cs.primary, size: 18),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'رحلة #${ride.rideId}',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: cs.onSurface),
),
Text(
'${ride.date} • ${ride.time}',
style: TextStyle(
fontSize: 11,
color: cs.onSurfaceVariant,
fontWeight: FontWeight.w600),
),
],
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 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: [
Icon(Icons.my_location_rounded,
size: 14, color: cs.success),
Container(
width: 2,
height: 16,
color: cs.outline,
margin: const EdgeInsets.symmetric(vertical: 2)),
Icon(Icons.location_on_rounded,
size: 14, color: cs.danger),
],
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ride.startLocation,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: cs.onSurface),
),
const SizedBox(height: 12),
Text(
ride.endLocation,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: cs.onSurface),
),
],
),
),
],
),
const SizedBox(height: 16),
// 3. Driver & Passenger (Slim Rows)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color:Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildSlimUserRow(
icon: Icons.local_taxi_rounded,
title: 'السائق',
name: ride.driverName,
phone: ride.driverPhone,
color: cs.info,
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} ل.س',
cs.success),
const SizedBox(width: 8),
_buildInfoChip(
Icons.straighten_rounded,
'${double.tryParse(ride.distance)?.toStringAsFixed(1) ?? 0} كم',
cs.warning),
const SizedBox(width: 8),
_buildInfoChip(
Icons.access_time_rounded,
ride.time.length > 5
? ride.time.substring(0, 5)
: ride.time,
cs.info),
],
),
// 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: cs.danger.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: cs.danger.withValues(alpha: 0.2)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline_rounded,
size: 16, color: cs.danger),
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,
}) {
final cs = Theme.of(context).colorScheme;
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: cs.onSurfaceVariant,
fontWeight: FontWeight.bold),
),
const SizedBox(width: 6),
Expanded(
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: cs.onSurface),
),
),
Text(
displayPhone,
style: TextStyle(
fontSize: 11, color: cs.onSurfaceVariant, 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.withValues(alpha: 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) {
final cs = Theme.of(context).colorScheme;
if (status == 'Begin' || status == 'Arrived') {
return cs.success;
}
if (status == 'Finished') return const Color(0xFF14B8A6);
if (status.contains('Cancel') || status == 'TimeOut') {
return cs.danger;
}
if (status == 'New') return cs.info;
return cs.onSurfaceVariant;
}
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) {
final cs = Theme.of(context).colorScheme;
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: [
MapTiles.layer(context),
// Route Polyline
if (startPos != null && endPos != null)
PolylineLayer(
polylines: [
Polyline(
points: [startPos!, endPos!],
strokeWidth: 5,
color: cs.primary.withValues(alpha: 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.withValues(alpha: 0.2),
blurRadius: 5),
],
),
child: Icon(Icons.flag_rounded,
color: cs.success, 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.withValues(alpha: 0.2),
blurRadius: 5),
],
),
child: Icon(Icons.location_on_rounded,
color: cs.danger, 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: cs.info.withValues(alpha: 0.3),
blurRadius: 8),
],
),
child: Icon(Icons.directions_car_rounded,
color: cs.info, 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.withValues(alpha: 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: cs.info,
),
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,
}) {
final cs = Theme.of(context).colorScheme;
return Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withValues(alpha: 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: cs.onSurfaceVariant,
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: cs.success.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child:
Icon(Icons.call_rounded, size: 20, color: cs.success),
),
),
],
);
}
}