Initial commit for intaleq_admin

This commit is contained in:
Hamza-Ayed
2026-01-20 23:39:59 +03:00
parent 0b17f93aaa
commit a367bc7e5c
53 changed files with 20383 additions and 14662 deletions

View File

@@ -0,0 +1,219 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_admin1/controller/functions/crud.dart';
import 'package:sefer_admin1/controller/functions/wallet.dart'; // تأكد من المسار
// --- Controller: المسؤول عن المنطق (البحث، الفحص، الإضافة) ---
class DriverGiftCheckerController extends GetxController {
// للتحكم في حقل النص
final TextEditingController phoneController = TextEditingController();
// لعرض النتائج وحالة التحميل
var statusLog = "".obs;
var isLoading = false.obs;
// قائمة السائقين (سنقوم بتحميلها للبحث عن الـ ID)
List<dynamic> driversCache = [];
@override
void onInit() {
super.onInit();
// fetchDriverCache(); // تحميل البيانات عند فتح الصفحة
}
// 1. تحميل قائمة السائقين لاستخراج الـ ID منها
Future<void> fetchDriverCache() async {
try {
final response = await CRUD().post(
link:
'https://api.intaleq.xyz/intaleq/Admin/driver/getDriverGiftPayment.php',
payload: {'phone': phoneController.text.trim()},
);
// print('response: ${response}');
if (response != 'failure') {
driversCache = (response['message']);
}
} catch (e) {
debugPrint("Error loading cache: $e");
}
}
// --- الدالة الرئيسية التي تنفذ العملية المطلوبة ---
Future<void> processDriverGift() async {
String phoneInput = phoneController.text.trim();
if (phoneInput.isEmpty) {
Get.snackbar("تنبيه", "يرجى إدخال رقم الهاتف",
backgroundColor: Colors.orange);
return;
}
await fetchDriverCache();
isLoading.value = true;
statusLog.value = "جاري البحث عن السائق...";
try {
// الخطوة 1: استخراج الـ ID بناءً على رقم الهاتف
var driver = driversCache.firstWhere(
(d) => d['phone'].toString().contains(phoneInput),
orElse: () => null,
);
if (driver == null) {
statusLog.value = "❌ لم يتم العثور على سائق بهذا الرقم في الكاش.";
isLoading.value = false;
return;
}
String driverId = driver['id'].toString();
String driverName = driver['name_arabic'] ?? 'بدون اسم';
statusLog.value =
"✅ تم العثور على السائق: $driverName (ID: $driverId)\nجاري فحص رصيد الهدايا...";
// الخطوة 2: فحص السيرفر هل الهدية موجودة؟
// bool hasGift = await _checkIfGiftExistsOnServer(driverId);
// if (hasGift) {
// statusLog.value +=
// "\n⚠ هذا السائق لديه هدية الافتتاح (30,000) مسبقاً. لم يتم اتخاذ إجراء.";
// } else {
// الخطوة 3: إضافة الهدية
statusLog.value += "\n🎁 الهدية غير موجودة. جاري الإضافة...";
await _addGiftToDriver(driverId, phoneInput, "30000");
// }
} catch (e) {
statusLog.value = "حدث خطأ غير متوقع: $e";
} finally {
isLoading.value = false;
}
}
// دالة إضافة الهدية باستخدام WalletController الموجود عندك
Future<void> _addGiftToDriver(
String driverId, String phone, String amount) async {
final wallet = Get.put(WalletController());
// استخدام الدالة الموجودة في نظامك
await wallet.addDrivergift3000('new driver', driverId, amount, phone);
// statusLog.value += "\n✅ تمت إضافة مبلغ $amount ل.س بنجاح!";
// إضافة تنبيه مرئي
// Get.snackbar("تم بنجاح", "تمت إضافة هدية الافتتاح للسائق",
// backgroundColor: Colors.green, colorText: Colors.white);
}
}
// --- View: واجهة المستخدم ---
class DriverGiftCheckPage extends StatelessWidget {
const DriverGiftCheckPage({super.key});
@override
Widget build(BuildContext context) {
// حقن الكنترولر
final controller = Get.put(DriverGiftCheckerController());
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC),
appBar: AppBar(
title: const Text("فحص ومنح هدية الافتتاح",
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: const Color(0xFF0F172A), // نفس لون الهيدر السابق
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
// كارد الإدخال
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 10)
],
),
child: Column(
children: [
const Icon(Icons.card_giftcard,
size: 50, color: Colors.amber),
const SizedBox(height: 10),
const Text(
"أدخل رقم الهاتف للتحقق",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// حقل الإدخال
TextField(
controller: controller.phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: 'مثال: 0912345678',
prefixIcon: const Icon(Icons.phone),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
filled: true,
fillColor: Colors.grey[50],
),
),
const SizedBox(height: 20),
// زر التنفيذ
Obx(() => SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.isLoading.value
? null
: () => controller.processDriverGift(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0F172A),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: controller.isLoading.value
? const CircularProgressIndicator(
color: Colors.white)
: const Text("تحقق ومنح الهدية (30,000)",
style: TextStyle(fontSize: 16)),
),
)),
],
),
),
const SizedBox(height: 30),
// منطقة عرض النتائج (Log)
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(12),
),
child: SingleChildScrollView(
child: Obx(() => Text(
controller.statusLog.value.isEmpty
? "بانتظار العملية..."
: controller.statusLog.value,
style: const TextStyle(
color: Colors.greenAccent,
fontFamily: 'monospace',
height: 1.5),
)),
),
),
),
],
),
),
);
}
}

View File

@@ -1,103 +1,654 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sefer_admin1/constant/links.dart';
import 'package:sefer_admin1/controller/functions/crud.dart';
import 'package:sefer_admin1/controller/functions/encrypt_decrypt.dart';
import 'package:get_storage/get_storage.dart'; // Ensure get_storage is in pubspec.yaml
import 'package:sefer_admin1/controller/functions/wallet.dart';
import 'package:sefer_admin1/views/widgets/elevated_btn.dart';
import 'package:sefer_admin1/views/widgets/my_scafold.dart';
import '../../../controller/drivers/driverthebest.dart';
import 'alexandria.dart';
import 'giza.dart';
// --- New Controller to handle the specific JSON URL ---
class DriverCacheController extends GetxController {
List<dynamic> drivers = [];
bool isLoading = false;
String lastUpdated = '';
String searchQuery = ''; // Search query state
class DriverTheBest extends StatelessWidget {
const DriverTheBest({super.key});
// Storage for paid drivers
final box = GetStorage();
List<String> paidDrivers = [];
@override
void onInit() {
super.onInit();
// Load previously paid drivers from storage
var stored = box.read('paid_drivers');
if (stored != null) {
paidDrivers = List<String>.from(stored.map((e) => e.toString()));
}
fetchData();
}
Future<void> fetchData() async {
isLoading = true;
update(); // Notify UI to show loader
try {
// Using GetConnect to fetch the JSON directly
final response = await GetConnect().get(
'https://api.intaleq.xyz/intaleq/ride/location/active_drivers_cache.json',
);
if (response.body != null && response.body is Map) {
if (response.body['data'] != null) {
drivers = List<dynamic>.from(response.body['data']);
}
if (response.body['last_updated'] != null) {
lastUpdated = response.body['last_updated'].toString();
}
}
} catch (e) {
debugPrint("Error fetching driver cache: $e");
} finally {
isLoading = false;
update(); // Update UI with data
}
}
// Update search query
void updateSearchQuery(String query) {
searchQuery = query;
update();
}
// Mark driver as paid and save to storage
void markAsPaid(String driverId) {
// Validation: Don't mark if ID is invalid
if (driverId == 'null' || driverId.isEmpty) return;
if (!paidDrivers.contains(driverId)) {
paidDrivers.add(driverId);
box.write('paid_drivers', paidDrivers);
update();
}
}
// Clear all paid status (Delete Box)
void clearPaidStorage() {
paidDrivers.clear();
box.remove('paid_drivers');
update();
Get.snackbar(
"Storage Cleared",
"Paid status history has been reset",
backgroundColor: Colors.redAccent,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
// Check if driver is already paid
bool isDriverPaid(String driverId) {
return paidDrivers.contains(driverId);
}
}
class DriverTheBestRedesigned extends StatelessWidget {
const DriverTheBestRedesigned({super.key});
@override
Widget build(BuildContext context) {
Get.put(Driverthebest(), permanent: true);
return MyScafolld(
title: 'Best Drivers'.tr,
body: [
GetBuilder<Driverthebest>(builder: (driverthebest) {
return driverthebest.driver.isNotEmpty
? Column(
// Put the new controller
final controller = Get.put(DriverCacheController());
return Scaffold(
backgroundColor: const Color(0xFFF8FAFC), // slate-50 background
body: SafeArea(
child: GetBuilder<DriverCacheController>(builder: (ctrl) {
if (ctrl.isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Filter List based on Search Query
List<dynamic> filteredDrivers = ctrl.drivers.where((driver) {
if (ctrl.searchQuery.isEmpty) return true;
final phone = driver['phone']?.toString() ?? '';
// Simple contains check for phone
return phone.contains(ctrl.searchQuery);
}).toList();
// Sort by Active Time (Hours) Descending
// We use the filtered list for sorting and display
filteredDrivers.sort((a, b) {
double hoursA = _calculateHoursFromStr(a['active_time']);
double hoursB = _calculateHoursFromStr(b['active_time']);
return hoursB.compareTo(hoursA);
});
// --- 1. Calculate Stats (Based on ALL drivers, not just filtered, to keep dashboard stable) ---
int totalDrivers = ctrl.drivers.length;
int eliteCount = 0;
int inactiveCount = 0;
double maxTime = 0.0;
for (var driver in ctrl.drivers) {
double hours = _calculateHoursFromStr(driver['active_time']);
if (hours > maxTime) maxTime = hours;
if (hours >= 50) {
eliteCount++;
} else if (hours < 5) {
inactiveCount++;
}
}
return Column(
children: [
// --- 2. Header (Slate-900 style) ---
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFF0F172A), // slate-900
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyElevatedButton(
title: 'Giza',
onPressed: () {
Get.to(() => DriverTheBestGiza());
}),
MyElevatedButton(
title: 'Alexandria',
onPressed: () {
Get.to(() => DriverTheBestAlexandria());
}),
],
),
SizedBox(
height: Get.height * .7,
child: ListView.builder(
itemCount: driverthebest.driver.length,
itemBuilder: (context, index) {
final driver = driverthebest.driver[index];
return ListTile(
leading: CircleAvatar(
child: Text(
(int.parse(driver['driver_count']) * 5 / 3600)
.toStringAsFixed(
0), // Perform division first, then convert to string
Row(
children: [
const Icon(Icons.local_taxi,
color: Colors.yellow, size: 24),
const SizedBox(width: 8),
Text(
'Best Drivers Dashboard'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
title:
Text((driver['name_arabic']) ?? 'Unknown Name'),
subtitle:
Text('Phone: ${(driver['phone']) ?? 'N/A'}'),
trailing: IconButton(
onPressed: () async {
Get.defaultDialog(
title:
'are you sure to pay to this driver gift'
.tr,
middleText: '',
onConfirm: () async {
final wallet =
Get.put(WalletController());
await wallet.addPaymentToDriver(
'200',
driver['id'].toString(),
driver['token']);
await wallet.addSeferWallet(
'200', driver['id'].toString());
await CRUD().post(
link: AppLink.deleteRecord,
payload: {
'driver_id': driver['id'].toString()
});
driverthebest.driver.removeAt(index);
driverthebest.update();
Get.back();
},
onCancel: () => Get.back());
},
icon: const Icon(Icons.wallet_giftcard_rounded),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.access_time,
color: Colors.grey, size: 12),
const SizedBox(width: 4),
Text(
ctrl.lastUpdated.isNotEmpty
? 'Updated: ${ctrl.lastUpdated}'
: 'Data Live',
style: TextStyle(
color: Colors.grey[400], fontSize: 12),
),
);
},
],
),
],
),
// Action Buttons (Delete Box & Refresh)
Row(
children: [
// Delete Box Icon
IconButton(
onPressed: () {
Get.defaultDialog(
title: "Reset Paid Status",
middleText:
"Are you sure you want to clear the list of paid drivers? This cannot be undone.",
textConfirm: "Yes, Clear",
textCancel: "Cancel",
confirmTextColor: Colors.white,
buttonColor: Colors.red,
onConfirm: () {
ctrl.clearPaidStorage();
Get.back();
},
);
},
icon: const Icon(Icons.delete_forever,
color: Colors.redAccent),
tooltip: "Clear Paid Storage",
style: IconButton.styleFrom(
backgroundColor: Colors.white10),
),
const SizedBox(width: 8),
IconButton(
onPressed: () {
ctrl.fetchData();
},
icon: const Icon(Icons.refresh,
color: Colors.blueAccent),
style: IconButton.styleFrom(
backgroundColor: Colors.white10),
),
],
)
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- 3. Statistics Cards Grid ---
SizedBox(
height: 100, // Fixed height for cards
child: Row(
children: [
Expanded(
child: _buildStatCard('Total',
totalDrivers.toString(), Colors.blue)),
const SizedBox(width: 8),
Expanded(
child: _buildStatCard('Elite',
eliteCount.toString(), Colors.amber)),
],
),
),
const SizedBox(height: 8),
SizedBox(
height: 100,
child: Row(
children: [
Expanded(
child: _buildStatCard('Inactive',
inactiveCount.toString(), Colors.red)),
const SizedBox(width: 8),
Expanded(
child: _buildStatCard(
'Max Time',
'${maxTime.toStringAsFixed(1)}h',
Colors.green)),
],
),
),
const SizedBox(height: 24),
// --- 4. Search Bar ---
TextField(
onChanged: (val) => ctrl.updateSearchQuery(val),
decoration: InputDecoration(
hintText: 'Search by phone number...',
prefixIcon:
const Icon(Icons.search, color: Colors.grey),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
),
),
const SizedBox(height: 16),
// --- 5. Driver List ---
if (filteredDrivers.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
ctrl.searchQuery.isNotEmpty
? "No drivers found with this number"
: "No drivers available",
style: TextStyle(color: Colors.grey[400])),
))
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredDrivers.length,
separatorBuilder: (c, i) =>
const SizedBox(height: 12),
itemBuilder: (context, index) {
final driver = filteredDrivers[index];
return _buildDriverCard(
context, driver, index, ctrl);
},
),
],
),
),
),
],
);
}),
),
);
}
// --- Helper Methods ---
// Updated to parse the Arabic string format "5 ساعة 30 دقيقة"
double _calculateHoursFromStr(dynamic activeTimeStr) {
if (activeTimeStr == null || activeTimeStr is! String) return 0.0;
try {
int hours = 0;
int mins = 0;
// Extract hours
final hoursMatch = RegExp(r'(\d+)\s*ساعة').firstMatch(activeTimeStr);
if (hoursMatch != null) {
hours = int.parse(hoursMatch.group(1) ?? '0');
}
// Extract minutes
final minsMatch = RegExp(r'(\d+)\s*دقيقة').firstMatch(activeTimeStr);
if (minsMatch != null) {
mins = int.parse(minsMatch.group(1) ?? '0');
}
return hours + (mins / 60.0);
} catch (e) {
return 0.0;
}
}
Widget _buildStatCard(String title, String value, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border(right: BorderSide(color: color, width: 4)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(title,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(value,
style: TextStyle(
fontSize: 22,
color: color.withOpacity(0.8),
fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildDriverCard(BuildContext context, Map driver, int index,
DriverCacheController controller) {
double hours = _calculateHoursFromStr(driver['active_time']);
String driverId = driver['id']?.toString() ?? 'null';
bool isPaid = controller.isDriverPaid(driverId);
// Determine Status Category (mimicking HTML logic)
String statusText;
Color statusColor;
if (hours >= 50) {
statusText = "Elite";
statusColor = Colors.amber;
} else if (hours >= 20) {
statusText = "Stable";
statusColor = Colors.green;
} else if (hours >= 5) {
statusText = "Experimental";
statusColor = Colors.blue;
} else {
statusText = "Inactive";
statusColor = Colors.red;
}
// Override colors if paid
Color cardBackground = isPaid ? Colors.teal.shade50 : Colors.white;
Color borderColor = isPaid ? Colors.teal : Colors.transparent;
// Calculate progress (max assumed 60 hours for 100% bar)
double progress = (hours / 60).clamp(0.0, 1.0);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cardBackground,
border: isPaid ? Border.all(color: borderColor, width: 2) : null,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4))
],
),
child: Column(
children: [
if (isPaid)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.teal,
borderRadius: BorderRadius.circular(4)),
child: const Text("PAID",
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold)),
)
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Avatar
CircleAvatar(
backgroundColor: statusColor.withOpacity(0.1),
radius: 24,
child: Text(
hours.toStringAsFixed(0),
style: TextStyle(
color: statusColor, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 12),
// Name and Phone
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
driver['name_arabic'] ?? 'Unknown Name',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: isPaid
? Colors.teal.shade900
: const Color(0xFF334155)),
),
const SizedBox(height: 4),
Text(
driver['phone'] ?? 'N/A',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Colors.grey),
),
const SizedBox(height: 2),
Text(
driver['active_time'] ?? '',
style: TextStyle(fontSize: 10, color: Colors.grey[400]),
),
],
)
: const Center(
child: Text('No drivers available.'),
);
})
],
isleading: true,
),
),
// Status Badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: statusColor.withOpacity(0.2)),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 10,
color: statusColor,
fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 12),
// Progress Bar
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Performance",
style: TextStyle(fontSize: 10, color: Colors.grey[600])),
Text("${hours.toStringAsFixed(2)} hrs",
style: const TextStyle(
fontSize: 10, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[100],
color: statusColor,
minHeight: 6,
borderRadius: BorderRadius.circular(3),
),
],
),
const SizedBox(height: 16),
const Divider(height: 1),
const SizedBox(height: 8),
// Actions Row
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Pay Gift Button (The specific request)
isPaid
? const Text("Payment Completed",
style: TextStyle(
color: Colors.teal, fontWeight: FontWeight.bold))
: ElevatedButton.icon(
onPressed: () {
_showPayDialog(driver, controller);
},
icon: const Icon(Icons.card_giftcard, size: 16),
label: Text("Pay Gift".tr),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo, // Dark blue/purple
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
),
],
)
],
),
);
}
void _showPayDialog(Map driver, DriverCacheController controller) {
// Check for valid ID immediately
String driverId = driver['driver_id']?.toString() ?? '';
String phone = driver['phone']?.toString() ?? '';
if (driverId.isEmpty || driverId == 'null') {
Get.snackbar("Error", "Cannot pay driver with missing ID",
backgroundColor: Colors.red, colorText: Colors.white);
return;
}
// Controller for the Amount Field
final TextEditingController amountController =
TextEditingController(text: '50000');
Get.defaultDialog(
title: 'Confirm Payment',
titleStyle: const TextStyle(
color: Color(0xFF0F172A), fontWeight: FontWeight.bold),
content: Column(
children: [
const Icon(Icons.wallet_giftcard, size: 50, color: Colors.indigo),
const SizedBox(height: 10),
Text(
'Sending gift to ${driver['name_arabic']}',
textAlign: TextAlign.center,
),
const SizedBox(height: 15),
// Amount Field
TextFormField(
controller: amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Amount (SYP)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
),
),
],
),
textConfirm: 'Pay Now',
confirmTextColor: Colors.white,
buttonColor: Colors.indigo,
onConfirm: () async {
final wallet = Get.put(WalletController());
// Get amount from field
String amount = amountController.text.trim();
if (amount.isEmpty) amount = '0';
// String driverToken = driver['token'] ?? '';
// 1. Add Payment
await wallet.addDriverWallet('gift_connect', driverId, amount, phone);
// 2. Add to Sefer Wallet
//await wallet.addSeferWallet(amount, driverId);
// 3. Delete Record via CRUD
// await CRUD()
// .post(link: AppLink.deleteRecord, payload: {'driver_id': driverId});
// 4. UI Update & Storage
// Mark as paid instead of removing completely, so we can see the color change
controller.markAsPaid(driverId);
Get.back(); // Close Dialog
Get.snackbar("Success", "Payment of $amount EGP sent to driver",
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM);
},
textCancel: 'Cancel',
onCancel: () => Get.back(),
);
}
}

View File

@@ -0,0 +1,448 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart'; // ضروري من أجل الاتصال
import '../../../constant/box_name.dart';
import '../../../main.dart';
class IntaleqTrackerScreen extends StatefulWidget {
const IntaleqTrackerScreen({super.key});
@override
State<IntaleqTrackerScreen> createState() => _IntaleqTrackerScreenState();
}
class _IntaleqTrackerScreenState extends State<IntaleqTrackerScreen> {
// === Map Controller ===
final MapController _mapController = MapController();
List<Marker> _markers = [];
// === State Variables ===
bool isLiveMode = true;
bool isLoading = false;
String lastUpdated = "جاري التحميل...";
// === Counters ===
int liveCount = 0;
int dayCount = 0;
Timer? _timer;
// === Admin Info ===
String myPhone = box.read(BoxName.adminPhone).toString();
bool get isSuperAdmin =>
myPhone == '963942542053' || myPhone == '963992952235';
// === URLs ===
final String _baseDir = "https://api.intaleq.xyz/intaleq/ride/location/";
@override
void initState() {
super.initState();
fetchData();
// === تعديل 1: التحديث كل 5 دقائق بدلاً من 15 ثانية ===
_timer = Timer.periodic(const Duration(minutes: 5), (timer) {
if (mounted) fetchData();
});
}
@override
void dispose() {
_timer?.cancel();
_mapController.dispose();
super.dispose();
}
// === دالة إجراء الاتصال ===
Future<void> _makePhoneCall(String phoneNumber) async {
final Uri launchUri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(launchUri)) {
await launchUrl(launchUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("لا يمكن إجراء الاتصال لهذا الرقم")),
);
}
}
// === Fetch Data Function ===
Future<void> fetchData() async {
if (!mounted) return;
setState(() => isLoading = true);
try {
// 1. طلب التحديث من PHP
String updateUrl =
"${_baseDir}getUpdatedLocationForAdmin.php?mode=${isLiveMode ? 'live' : 'day'}";
await http.get(Uri.parse(updateUrl));
String v = DateTime.now().millisecondsSinceEpoch.toString();
// === Live Data ===
final responseLive =
await http.get(Uri.parse("${_baseDir}locations_live.json?v=$v"));
if (responseLive.statusCode == 200) {
final data = json.decode(responseLive.body);
List drivers = (data is Map && data.containsKey('drivers'))
? data['drivers']
: data;
setState(() {
liveCount = drivers.length;
if (isLiveMode) _buildMarkers(drivers);
});
}
// === Day Data ===
final responseDay =
await http.get(Uri.parse("${_baseDir}locations_day.json?v=$v"));
if (responseDay.statusCode == 200) {
final data = json.decode(responseDay.body);
List drivers = (data is Map && data.containsKey('drivers'))
? data['drivers']
: data;
setState(() {
dayCount = drivers.length;
if (!isLiveMode) _buildMarkers(drivers);
});
}
setState(() {
lastUpdated = DateTime.now().toString().substring(11, 19);
});
} catch (e) {
print("Exception: $e");
setState(() => lastUpdated = "خطأ في الاتصال");
} finally {
if (mounted) setState(() => isLoading = false);
}
}
// === Build Markers ===
void _buildMarkers(List<dynamic> drivers) {
List<Marker> newMarkers = [];
for (var d in drivers) {
double lat = double.tryParse((d['lat'] ?? "0").toString()) ?? 0.0;
double lon = double.tryParse((d['lon'] ?? "0").toString()) ?? 0.0;
double heading = double.tryParse((d['heading'] ?? "0").toString()) ?? 0.0;
String id = (d['id'] ?? "Unknown").toString();
String speed = (d['speed'] ?? "0").toString();
String name = (d['name'] ?? "كابتن").toString();
String phone = (d['phone'] ?? "").toString();
String completed = (d['completed'] ?? "0").toString();
String cancelled = (d['cancelled'] ?? "0").toString();
if (lat != 0 && lon != 0) {
newMarkers.add(
Marker(
point: LatLng(lat, lon),
width: 50,
height: 50,
child: GestureDetector(
onTap: () {
_showDriverInfoDialog(
driverId: id,
name: name,
phone: phone,
speed: speed,
heading: heading,
completed: completed,
cancelled: cancelled,
);
},
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
shape: BoxShape.circle,
boxShadow: const [
BoxShadow(blurRadius: 3, color: Colors.black26)
]),
),
Transform.rotate(
angle: heading * (math.pi / 180),
child: Icon(
Icons.navigation,
color: isLiveMode
? const Color(0xFF27AE60)
: const Color(0xFF2980B9),
size: 28,
),
),
],
),
),
),
);
}
}
setState(() {
_markers = newMarkers;
});
}
// === Dialog Function ===
void _showDriverInfoDialog({
required String driverId,
required String name,
required String phone,
required String speed,
required double heading,
required String completed,
required String cancelled,
}) {
showDialog(
context: context,
builder: (_) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("بيانات الكابتن",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50))),
const Divider(thickness: 1, height: 25),
_infoRow(Icons.person, "الاسم", name),
_infoRow(Icons.badge, "المعرف (ID)", driverId),
_infoRow(Icons.speed, "السرعة", "$speed كم/س"),
const SizedBox(height: 10),
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("مكتملة", completed, Colors.green),
Container(
width: 1, height: 30, color: Colors.grey.shade300),
_statItem("ملغاة", cancelled, Colors.red),
],
),
),
// === تعديل 2: جعل رقم الهاتف قابلاً للنقر ===
if (isSuperAdmin) ...[
const SizedBox(height: 15),
InkWell(
onTap: () {
if (phone.isNotEmpty) _makePhoneCall(phone);
},
borderRadius: BorderRadius.circular(8),
child: Container(
padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
decoration: BoxDecoration(
color: const Color(0xFFFFF3CD),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFFEEBA))),
child: Row(
children: [
Expanded(
child: _infoRow(Icons.phone, "الهاتف", phone,
isPrivate: true)),
const SizedBox(width: 5),
const Icon(Icons.call, color: Colors.green, size: 20),
],
),
),
),
],
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2C3E50),
foregroundColor: Colors.white,
),
child: const Text("إغلاق"),
),
)
],
),
),
),
);
}
// Helper Widgets
Widget _infoRow(IconData icon, String label, String value,
{bool isPrivate = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon,
size: 20,
color: isPrivate ? Colors.orange[800] : Colors.grey[600]),
const SizedBox(width: 8),
Text("$label: ",
style:
const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
Expanded(
child: Text(value,
style: TextStyle(
fontSize: 14,
fontWeight:
isPrivate ? FontWeight.bold : FontWeight.normal),
textAlign: TextAlign.end)),
],
),
);
}
Widget _statItem(String label, String val, Color color) {
return Column(
children: [
Text(val,
style: TextStyle(
color: color, fontWeight: FontWeight.bold, fontSize: 16)),
Text(label, style: const TextStyle(fontSize: 11, color: Colors.grey)),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("نظام تتبع الكباتن"),
backgroundColor: const Color(0xFF2C3E50),
foregroundColor: Colors.white),
body: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: const MapOptions(
initialCenter: LatLng(33.513, 36.276), initialZoom: 10.0),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.tripz.app'),
MarkerLayer(markers: _markers),
],
),
// === Dashboard ===
Positioned(
top: 20,
right: 15,
child: Container(
width: 260,
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(color: Colors.black12, blurRadius: 10)
]),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text("لوحة التحكم",
style: TextStyle(fontWeight: FontWeight.bold)),
const Divider(),
// أزرار التبديل
Row(
children: [
Expanded(
child: _modeBtn("أرشيف اليوم", !isLiveMode, () {
setState(() => isLiveMode = false);
fetchData();
})),
const SizedBox(width: 8),
Expanded(
child: _modeBtn("مباشر", isLiveMode, () {
setState(() => isLiveMode = true);
fetchData();
})),
],
),
const SizedBox(height: 15),
// === عرض العدادين معاً ===
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("$liveCount",
style: const TextStyle(
color: Color(0xFF27AE60),
fontWeight: FontWeight.bold,
fontSize: 14)),
const Text("نشط الآن (مباشر):",
style: TextStyle(fontSize: 12)),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("$dayCount",
style: const TextStyle(
color: Color(0xFF2980B9),
fontWeight: FontWeight.bold,
fontSize: 14)),
const Text("إجمالي اليوم:",
style: TextStyle(fontSize: 12)),
],
),
const SizedBox(height: 10),
Text(isLoading ? "جاري التحديث..." : "تحديث: $lastUpdated",
style: const TextStyle(fontSize: 10, color: Colors.grey)),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoading ? null : fetchData,
child: const Text("تحديث البيانات")))
],
),
),
),
],
),
);
}
Widget _modeBtn(String title, bool active, VoidCallback onTap) {
return InkWell(
onTap: onTap,
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: active ? const Color(0xFF3498DB) : Colors.white,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF3498DB))),
child: Text(title,
style: TextStyle(
color: active ? Colors.white : const Color(0xFF3498DB),
fontWeight: active ? FontWeight.bold : FontWeight.normal)),
),
);
}
}

View File

@@ -0,0 +1,557 @@
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';
// Keep your specific imports
import 'package:sefer_admin1/controller/functions/crud.dart';
import 'package:sefer_admin1/print.dart';
/// --------------------------------------------------------------------------
/// 1. DATA MODELS
/// --------------------------------------------------------------------------
class DriverLocation {
final double latitude;
final double longitude;
final double speed;
final double heading;
final String updatedAt;
DriverLocation({
required this.latitude,
required this.longitude,
required this.speed,
required this.heading,
required this.updatedAt,
});
factory DriverLocation.fromJson(Map<String, dynamic> json) {
return DriverLocation(
latitude: double.tryParse(json['latitude'].toString()) ?? 0.0,
longitude: double.tryParse(json['longitude'].toString()) ?? 0.0,
speed: double.tryParse(json['speed'].toString()) ?? 0.0,
heading: double.tryParse(json['heading'].toString()) ?? 0.0,
updatedAt: json['updated_at'] ?? '',
);
}
}
/// --------------------------------------------------------------------------
/// 2. GETX CONTROLLER
/// --------------------------------------------------------------------------
class RideMonitorController extends GetxController {
// CONFIGURATION
final String apiUrl =
"https://api.intaleq.xyz/intaleq/Admin/rides/monitorRide.php";
// INPUT CONTROLLERS
final TextEditingController phoneInputController = TextEditingController();
// OBSERVABLES
var isTracking = false.obs;
var isLoading = false.obs;
var hasError = false.obs;
var errorMessage = ''.obs;
// Driver & Ride Data
var driverLocation = Rxn<DriverLocation>();
var driverName = "Unknown Driver".obs;
var rideStatus = "Waiting...".obs;
// Route Data
var startPoint = Rxn<LatLng>();
var endPoint = Rxn<LatLng>();
var routePolyline = <LatLng>[].obs; // List of points for the line
// Map Variables
final MapController mapController = MapController();
Timer? _timer;
bool _isFirstLoad = true; // To trigger auto-fit bounds only on first success
@override
void onClose() {
_timer?.cancel();
phoneInputController.dispose();
super.onClose();
}
// --- ACTIONS ---
void startSearch() {
if (phoneInputController.text.trim().isEmpty) {
Get.snackbar("Error", "Please enter a phone number");
return;
}
// Reset state
hasError.value = false;
errorMessage.value = '';
driverLocation.value = null;
startPoint.value = null;
endPoint.value = null;
routePolyline.clear();
driverName.value = "Loading...";
rideStatus.value = "Loading...";
_isFirstLoad = true;
// Switch UI
isTracking.value = true;
isLoading.value = true;
// Start fetching
fetchRideData();
// Start Polling
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 10), (timer) {
fetchRideData();
});
}
void stopTracking() {
_timer?.cancel();
isTracking.value = false;
isLoading.value = false;
phoneInputController.clear();
}
Future<void> fetchRideData() async {
final phone = phoneInputController.text.trim();
if (phone.isEmpty) return;
try {
final response = await CRUD().post(
link: apiUrl,
payload: {"phone": phone},
);
// Log.print('response: ${response}');
if (response != 'failure') {
final jsonResponse = response;
if ((jsonResponse['message'] != null &&
jsonResponse['message'] != 'failure') ||
jsonResponse['status'] == 'success') {
final data =
jsonResponse['message'] ?? jsonResponse['data'] ?? jsonResponse;
// 1. Parse Driver Info
if (data['driver_details'] != null) {
driverName.value = data['driver_details']['fullname'] ?? "Unknown";
}
// 2. Parse Ride Info & Route
if (data['ride_details'] != null) {
rideStatus.value = data['ride_details']['status'] ?? "Unknown";
// Parse Start/End Locations (Format: "lat,lng")
String? startStr = data['ride_details']['start_location'];
String? endStr = data['ride_details']['end_location'];
LatLng? s = _parseLatLngString(startStr);
LatLng? e = _parseLatLngString(endStr);
if (s != null && e != null) {
startPoint.value = s;
endPoint.value = e;
routePolyline.value = [s, e]; // Straight line for now
}
}
// 3. Parse Live Location
final locData = data['driver_location'];
if (locData is Map<String, dynamic>) {
final newLocation = DriverLocation.fromJson(locData);
driverLocation.value = newLocation;
// 4. Update Camera Bounds
_updateMapBounds();
} else {
// Even if no live driver, we might want to show the route
if (startPoint.value != null && endPoint.value != null) {
_updateMapBounds();
}
print("No live location coordinates.");
}
hasError.value = false;
} else {
hasError.value = true;
errorMessage.value = jsonResponse['message'] ??
"Phone number not found or no active ride.";
}
} else {
hasError.value = true;
errorMessage.value = "Connection Failed";
}
} catch (e) {
print("Polling Error: $e");
if (isLoading.value) {
hasError.value = true;
errorMessage.value = e.toString();
}
} finally {
isLoading.value = false;
}
}
// Helper to parse "lat,lng" string
LatLng? _parseLatLngString(String? str) {
if (str == null || !str.contains(',')) return null;
try {
final parts = str.split(',');
final lat = double.parse(parts[0].trim());
final lng = double.parse(parts[1].trim());
return LatLng(lat, lng);
} catch (e) {
print("Error parsing location string '$str': $e");
return null;
}
}
// Logic to fit start, end, and driver on screen
void _updateMapBounds() {
// Only auto-fit on the first successful load to avoid fighting user pan/zoom
if (!_isFirstLoad) return;
List<LatLng> pointsToFit = [];
if (startPoint.value != null) pointsToFit.add(startPoint.value!);
if (endPoint.value != null) pointsToFit.add(endPoint.value!);
if (driverLocation.value != null) {
pointsToFit.add(LatLng(
driverLocation.value!.latitude, driverLocation.value!.longitude));
}
if (pointsToFit.isNotEmpty) {
try {
final bounds = LatLngBounds.fromPoints(pointsToFit);
mapController.fitCamera(
CameraFit.bounds(
bounds: bounds,
padding:
const EdgeInsets.all(80.0), // Padding so markers aren't on edge
),
);
_isFirstLoad = false; // Disable auto-fit after initial success
} catch (e) {
print("Map Controller not ready yet: $e");
}
}
}
}
/// --------------------------------------------------------------------------
/// 3. UI SCREEN
/// --------------------------------------------------------------------------
class RideMonitorScreen extends StatelessWidget {
const RideMonitorScreen({super.key});
@override
Widget build(BuildContext context) {
final RideMonitorController controller = Get.put(RideMonitorController());
return Scaffold(
appBar: AppBar(
title: const Text("Admin Ride Monitor"),
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
actions: [
Obx(() => controller.isTracking.value
? IconButton(
icon: const Icon(Icons.close),
onPressed: controller.stopTracking,
tooltip: "Stop Tracking",
)
: const SizedBox.shrink()),
],
),
body: Obx(() {
if (!controller.isTracking.value) {
return _buildSearchForm(context, controller);
}
return _buildMapTrackingView(controller);
}),
);
}
Widget _buildSearchForm(
BuildContext context, RideMonitorController controller) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.map_outlined, size: 80, color: Colors.blueAccent),
const SizedBox(height: 20),
const Text(
"Track Active Ride",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
const Text(
"Enter Driver or Passenger Phone Number",
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 30),
TextField(
controller: controller.phoneInputController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: "Phone Number",
hintText: "e.g. 9639...",
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
prefixIcon: const Icon(Icons.phone),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.startSearch,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: const Text("Start Monitoring",
style: TextStyle(color: Colors.white, fontSize: 16)),
),
),
],
),
),
);
}
Widget _buildMapTrackingView(RideMonitorController controller) {
return Stack(
children: [
FlutterMap(
mapController: controller.mapController,
options: MapOptions(
initialCenter: const LatLng(30.0444, 31.2357),
initialZoom: 12.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.sefer.admin',
),
// 1. ROUTE LINE (Polyline)
if (controller.routePolyline.isNotEmpty)
PolylineLayer(
polylines: [
Polyline(
points: controller.routePolyline.value,
strokeWidth: 5.0,
color: Colors.blueAccent.withOpacity(0.8),
borderStrokeWidth: 2.0,
borderColor: Colors.blue[900]!,
),
],
),
// 2. START & END MARKERS
MarkerLayer(
markers: [
// Start Point (Green Flag)
if (controller.startPoint.value != null)
Marker(
point: controller.startPoint.value!,
width: 40,
height: 40,
child:
const Icon(Icons.flag, color: Colors.green, size: 40),
alignment: Alignment.topCenter,
),
// End Point (Red Flag)
if (controller.endPoint.value != null)
Marker(
point: controller.endPoint.value!,
width: 40,
height: 40,
child: const Icon(Icons.flag, color: Colors.red, size: 40),
alignment: Alignment.topCenter,
),
// Driver Car Marker
if (controller.driverLocation.value != null)
Marker(
point: LatLng(
controller.driverLocation.value!.latitude,
controller.driverLocation.value!.longitude,
),
width: 60,
height: 60,
child: Transform.rotate(
angle: (controller.driverLocation.value!.heading *
(3.14159 / 180)),
child: Column(
children: [
const Icon(
Icons.directions_car_filled,
color: Colors
.black, // Dark car for visibility on blue line
size: 35,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.8),
borderRadius: BorderRadius.circular(4),
),
child: Text(
"${controller.driverLocation.value!.speed.toInt()} km",
style: const TextStyle(
fontSize: 10, fontWeight: FontWeight.bold),
),
)
],
),
),
),
],
),
],
),
// LOADING OVERLAY
if (controller.isLoading.value &&
controller.driverLocation.value == null &&
controller.startPoint.value == null)
Container(
color: Colors.black45,
child: const Center(
child: CircularProgressIndicator(color: Colors.white)),
),
// ERROR OVERLAY
if (controller.hasError.value)
Center(
child: Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(blurRadius: 10, color: Colors.black26)
]),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 40),
const SizedBox(height: 10),
Text(controller.errorMessage.value,
textAlign: TextAlign.center),
const SizedBox(height: 10),
ElevatedButton(
onPressed: controller.stopTracking,
child: const Text("Back"))
],
),
),
),
// INFO CARD
if (!controller.hasError.value && !controller.isLoading.value)
Positioned(
bottom: 20,
left: 20,
right: 20,
child: Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const CircleAvatar(
backgroundColor: Colors.blueAccent,
child: Icon(Icons.person, color: Colors.white),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.driverName.value,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Icon(Icons.circle,
size: 10,
color:
controller.rideStatus.value == 'Begin'
? Colors.green
: Colors.grey),
const SizedBox(width: 5),
Text(controller.rideStatus.value,
style: const TextStyle(
fontWeight: FontWeight.w600)),
],
),
],
),
),
],
),
const Divider(),
if (controller.driverLocation.value != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildInfoBadge(Icons.speed,
"${controller.driverLocation.value!.speed.toStringAsFixed(1)} km/h"),
_buildInfoBadge(
Icons.access_time,
controller.driverLocation.value!.updatedAt
.split(' ')
.last),
],
)
else
const Text("Connecting to driver...",
style: TextStyle(
color: Colors.orange,
fontStyle: FontStyle.italic)),
],
),
),
),
),
],
);
}
Widget _buildInfoBadge(IconData icon, String text) {
return Row(
children: [
Icon(icon, size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(text,
style: TextStyle(
color: Colors.grey[800], fontWeight: FontWeight.bold)),
],
);
}
}