655 lines
23 KiB
Dart
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(),
|
|
);
|
|
}
|
|
}
|