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

655 lines
23 KiB
Dart

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<dynamic> drivers = [];
bool isLoading = false;
String lastUpdated = '';
String searchQuery = ''; // Search query state
// 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) {
// 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: [
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(),
);
}
}