import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; // Ensure get_storage is in pubspec.yaml import 'package:sefer_admin1/controller/functions/wallet.dart'; // --- New Controller to handle the specific JSON URL --- class DriverCacheController extends GetxController { List drivers = []; bool isLoading = false; String lastUpdated = ''; String searchQuery = ''; // Search query state // Storage for paid drivers final box = GetStorage(); List paidDrivers = []; @override void onInit() { super.onInit(); // Load previously paid drivers from storage var stored = box.read('paid_drivers'); if (stored != null) { paidDrivers = List.from(stored.map((e) => e.toString())); } fetchData(); } Future 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.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) { // Put the new controller final controller = Get.put(DriverCacheController()); return Scaffold( backgroundColor: const Color(0xFFF8FAFC), // slate-50 background body: SafeArea( child: GetBuilder(builder: (ctrl) { if (ctrl.isLoading) { return const Center(child: CircularProgressIndicator()); } // Filter List based on Search Query List 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: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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, ), ), ], ), 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]), ), ], ), ), // 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(), ); } }