996 lines
42 KiB
Dart
996 lines
42 KiB
Dart
part of '../deliveries/deliveries.dart';
|
|
|
|
// -------------------------------------------------------------------------
|
|
// CART PAGE (Active Deliveries)
|
|
// -------------------------------------------------------------------------
|
|
class Cartpage extends StatefulWidget {
|
|
const Cartpage({super.key});
|
|
|
|
@override
|
|
State<Cartpage> createState() => _CartpageState();
|
|
}
|
|
|
|
class _CartpageState extends State<Cartpage> with AutomaticKeepAliveClientMixin {
|
|
final DeliveryProvider _provider = DeliveryProvider();
|
|
final CreateDeliveryLogProvider _deliveryLogProvider = CreateDeliveryLogProvider();
|
|
|
|
List<Map<String, dynamic>> _activeDeliveries = <Map<String, dynamic>>[];
|
|
StreamSubscription<void>? _pollerSubscription;
|
|
bool _fetching = false;
|
|
final Map<String, Timer> _deliveryTimers = <String, Timer>{};
|
|
final Map<String, Map<String, dynamic>> _deliveryBasePayload = <String, Map<String, dynamic>>{};
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchActive();
|
|
_startPolling();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pollerSubscription?.cancel();
|
|
_stopAllTimers();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startPolling() {
|
|
_pollerSubscription?.cancel();
|
|
_pollerSubscription = Stream.periodic(
|
|
const Duration(seconds: 10),
|
|
(_) {},
|
|
).asyncMap((_) async {
|
|
if (!_fetching && mounted) {
|
|
await _fetchActive();
|
|
}
|
|
}).listen(
|
|
(_) {},
|
|
onError: (error) {
|
|
debugPrint('[CART][STREAM ERROR] $error');
|
|
},
|
|
cancelOnError: false,
|
|
);
|
|
}
|
|
|
|
Future<void> _fetchActive() async {
|
|
if (_fetching) return;
|
|
|
|
_fetching = true;
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
|
|
|
if (userId == 0) {
|
|
debugPrint('[CART] No user ID found');
|
|
_fetching = false;
|
|
return;
|
|
}
|
|
|
|
// Get current date in YYYY-MM-DD format
|
|
final now = DateTime.now();
|
|
final today = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
|
|
|
// Hardcoded API endpoint for cart page only: v2/deliveries/getdeliveries
|
|
final bool isLive = ApiConstants.mainRoute == 'live';
|
|
final baseUrl = isLive
|
|
? 'https://jupiter.nearle.app/live/api/v2/deliveries/getdeliveries'
|
|
: 'https://jupiter.nearle.app/dev/api/v2/deliveries/getdeliveries';
|
|
|
|
final uri = Uri.parse(baseUrl).replace(queryParameters: {
|
|
'userid': userId.toString(),
|
|
'fromdate': today,
|
|
'todate': today,
|
|
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
|
});
|
|
|
|
debugPrint('[CART] Fetching from: $uri');
|
|
|
|
// Fetch deliveries from API directly using http
|
|
// Hardcoded endpoint for cart page: v2/deliveries/getdeliveries
|
|
final httpClient = http.Client();
|
|
List<dynamic> items = [];
|
|
try {
|
|
final response = await httpClient.get(uri);
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
final decoded = json.decode(response.body);
|
|
final data = decoded is Map<String, dynamic>
|
|
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
|
: decoded;
|
|
|
|
items = data is List
|
|
? data
|
|
: (data is Map && data['items'] is List ? data['items'] as List : []);
|
|
} else {
|
|
debugPrint('[CART] API error: ${response.statusCode}');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('[CART] Error fetching from API: $e');
|
|
} finally {
|
|
httpClient.close();
|
|
}
|
|
|
|
debugPrint('[CART] Raw API items count: ${items.length}');
|
|
|
|
// Debug: Print all order statuses to see what we're getting
|
|
for (final item in items) {
|
|
if (item is Map<String, dynamic>) {
|
|
final orderId = (item['orderid'] ?? '').toString();
|
|
final status = (item['orderstatus'] ?? '').toString();
|
|
debugPrint('[CART] Order $orderId has status: "$status" (raw: ${item['orderstatus']})');
|
|
}
|
|
}
|
|
|
|
// Filter for ACTIVE status only
|
|
final activeOrders = items
|
|
.whereType<Map<String, dynamic>>()
|
|
.where((order) {
|
|
final status = (order['orderstatus']?.toString().toLowerCase() ?? '').trim();
|
|
final isActive = status == 'active';
|
|
if (isActive) {
|
|
debugPrint('[CART] ✅ Found active order: ${order['orderid']}');
|
|
} else {
|
|
debugPrint('[CART] ❌ Order ${order['orderid']} has status: "$status" (not active)');
|
|
}
|
|
return isActive;
|
|
})
|
|
.toList();
|
|
|
|
debugPrint('[CART] Found ${activeOrders.length} active deliveries out of ${items.length} total');
|
|
|
|
// Sort by order ID or step number if available
|
|
activeOrders.sort((a, b) {
|
|
final stepA = (a['step'] ?? a['Step'] ?? 0).toString();
|
|
final stepB = (b['step'] ?? b['Step'] ?? 0).toString();
|
|
final stepAInt = int.tryParse(stepA) ?? 0;
|
|
final stepBInt = int.tryParse(stepB) ?? 0;
|
|
if (stepAInt != stepBInt) return stepAInt.compareTo(stepBInt);
|
|
|
|
final orderIdA = (a['orderid'] ?? '').toString();
|
|
final orderIdB = (b['orderid'] ?? '').toString();
|
|
return orderIdA.compareTo(orderIdB);
|
|
});
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_activeDeliveries = activeOrders;
|
|
});
|
|
}
|
|
|
|
// Get all active order IDs
|
|
final activeOrderIds = activeOrders
|
|
.map((o) => (o['orderid'] ?? '').toString())
|
|
.where((id) => id.isNotEmpty)
|
|
.toSet();
|
|
|
|
// ✅ CRITICAL: Start/restart timers for ALL active deliveries
|
|
// This ensures every active delivery posts logs every 30 seconds
|
|
for (final order in activeOrders) {
|
|
final orderId = (order['orderid'] ?? '').toString();
|
|
if (orderId.isEmpty) {
|
|
debugPrint('[CART] ⚠️ Skipping order with empty orderId');
|
|
continue;
|
|
}
|
|
|
|
// Always restart timer to ensure it's running (handles edge cases)
|
|
if (_deliveryTimers.containsKey(orderId)) {
|
|
debugPrint('[CART] 🔄 Restarting timer for active delivery: $orderId');
|
|
_deliveryTimers[orderId]?.cancel();
|
|
_deliveryTimers.remove(orderId);
|
|
// Also clear base payload to force reload
|
|
_deliveryBasePayload.remove(orderId);
|
|
}
|
|
|
|
debugPrint('[CART] ▶️ Starting timer for active delivery: $orderId');
|
|
await _startDeliveryPosting(order);
|
|
debugPrint('[CART] ✅ Timer started successfully for: $orderId');
|
|
}
|
|
|
|
// ✅ Stop timers for deliveries that are no longer active
|
|
final timersToStop = _deliveryTimers.keys
|
|
.where((id) => !activeOrderIds.contains(id))
|
|
.toList();
|
|
|
|
for (final id in timersToStop) {
|
|
debugPrint('[CART] Stopping timer for orderId: $id (no longer active)');
|
|
_stopDeliveryPosting(id);
|
|
}
|
|
|
|
debugPrint('[CART] Active timers: ${_deliveryTimers.keys.toList()}');
|
|
} catch (e) {
|
|
debugPrint('[CART] Error fetching active deliveries: $e');
|
|
} finally {
|
|
_fetching = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _startDeliveryPosting(Map<String, dynamic> order) async {
|
|
final orderId = (order['orderid'] ?? '').toString();
|
|
if (orderId.isEmpty) {
|
|
debugPrint('[CART][DELIVERYLOG] ⚠️ Cannot start timer: empty orderId');
|
|
return;
|
|
}
|
|
|
|
// Safety check: If timer already exists, cancel it first (shouldn't happen after cleanup above)
|
|
if (_deliveryTimers.containsKey(orderId)) {
|
|
debugPrint('[CART][DELIVERYLOG] ⚠️ Timer already exists for $orderId, canceling old one');
|
|
_deliveryTimers[orderId]?.cancel();
|
|
_deliveryTimers.remove(orderId);
|
|
}
|
|
|
|
debugPrint('[CART][DELIVERYLOG] 🚀 Starting 30-second timer for orderId: $orderId');
|
|
|
|
// Get starttime from SharedPreferences (saved when order became active via updateActiveStatus)
|
|
// If not found, use activetime from order data, or current time as fallback
|
|
String startTime = '';
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final deliveryId = (order['deliveryid'] ?? 0).toString();
|
|
|
|
// Method 1: Get from SharedPreferences (saved when order became active)
|
|
startTime = prefs.getString('delivery_starttime_$deliveryId') ?? '';
|
|
if (startTime.isNotEmpty) {
|
|
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from SharedPreferences: $startTime');
|
|
}
|
|
|
|
// Method 2: Fallback - try to get from order data (starttime field)
|
|
if (startTime.isEmpty) {
|
|
startTime = (order['starttime'] ?? order['startTime'] ?? '').toString();
|
|
if (startTime.isNotEmpty) {
|
|
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from order data: $startTime');
|
|
}
|
|
}
|
|
|
|
// Method 3: Fallback - try activetime from order data
|
|
if (startTime.isEmpty) {
|
|
final activetime = (order['activetime'] ?? order['activTime'] ?? '').toString();
|
|
if (activetime.isNotEmpty) {
|
|
startTime = activetime;
|
|
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from activetime: $startTime');
|
|
}
|
|
}
|
|
|
|
// Method 4: Last fallback - current time (shouldn't happen if updateActiveStatus was called)
|
|
if (startTime.isEmpty) {
|
|
final now = DateTime.now();
|
|
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
|
debugPrint('[CART][DELIVERYLOG] ⚠️ Using current time as starttime fallback: $startTime');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('[CART][DELIVERYLOG] ❌ Error getting starttime: $e');
|
|
// Set a fallback starttime even on error
|
|
final now = DateTime.now();
|
|
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
// CRITICAL: Ensure starttime is never empty
|
|
if (startTime.isEmpty) {
|
|
final now = DateTime.now();
|
|
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
|
debugPrint('[CART][DELIVERYLOG] ⚠️ Final fallback: starttime was empty, using: $startTime');
|
|
}
|
|
|
|
debugPrint('[CART][DELIVERYLOG] 📝 Final starttime for orderId $orderId: $startTime');
|
|
|
|
// Create base payload with starttime
|
|
final base = <String, dynamic>{
|
|
'logid': 0,
|
|
'tenantid': order['tenantid'] ?? 0,
|
|
'partnerid': order['partnerid'] ?? 0,
|
|
'locationid': order['locationid'] ?? 0,
|
|
'orderheaderid': order['orderheaderid'] ?? 0,
|
|
'deliveryid': order['deliveryid'] ?? 0,
|
|
'userid': order['userid'] ?? 0,
|
|
'orderid': orderId,
|
|
'orderstatus': 'active',
|
|
'starttime': startTime, // Include starttime in base payload
|
|
};
|
|
|
|
_deliveryBasePayload[orderId] = base;
|
|
|
|
// Save to SharedPreferences for persistence
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('deliverylog_${orderId}_tenantid', (base['tenantid'] ?? 0).toString());
|
|
await prefs.setString('deliverylog_${orderId}_partnerid', (base['partnerid'] ?? 0).toString());
|
|
await prefs.setString('deliverylog_${orderId}_locationid', (base['locationid'] ?? 0).toString());
|
|
await prefs.setString('deliverylog_${orderId}_orderheaderid', (base['orderheaderid'] ?? 0).toString());
|
|
await prefs.setString('deliverylog_${orderId}_deliveryid', (base['deliveryid'] ?? 0).toString());
|
|
await prefs.setString('deliverylog_${orderId}_userid', (base['userid'] ?? 0).toString());
|
|
await prefs.setString('deliverylog_${orderId}_orderid', orderId);
|
|
await prefs.setString('deliverylog_${orderId}_orderstatus', 'active');
|
|
await prefs.setString('deliverylog_${orderId}_starttime', startTime); // Save starttime
|
|
} catch (e) {
|
|
debugPrint('[CART][DELIVERYLOG] Error saving payload: $e');
|
|
}
|
|
|
|
// Post once immediately (don't await - let it run in background)
|
|
_postDeliveryLog(orderId, order);
|
|
debugPrint('[CART][DELIVERYLOG] 📤 Posted initial log for orderId: $orderId');
|
|
|
|
// Then every 30 seconds - CRITICAL: This ensures logs are posted every 30 seconds
|
|
final timer = Timer.periodic(const Duration(seconds: 30), (t) {
|
|
debugPrint('[CART][DELIVERYLOG] ⏰ Timer tick for orderId: $orderId (30 seconds elapsed)');
|
|
_postDeliveryLog(orderId, order);
|
|
});
|
|
|
|
_deliveryTimers[orderId] = timer;
|
|
debugPrint('[CART][DELIVERYLOG] ✅ Timer registered for orderId: $orderId (will post every 30 seconds)');
|
|
}
|
|
|
|
void _postDeliveryLog(String orderId, Map<String, dynamic> order) {
|
|
if (!mounted) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] Widget disposed, skipping');
|
|
return;
|
|
}
|
|
|
|
debugPrint('[CART][DELIVERYLOG][POST] ⏰ Posting log for orderId: $orderId at ${DateTime.now()}');
|
|
|
|
// Use Future.microtask to ensure the async operation runs independently
|
|
Future.microtask(() => _performPost(orderId));
|
|
}
|
|
|
|
Future<void> _performPost(String orderId) async {
|
|
try {
|
|
debugPrint('[CART][DELIVERYLOG][POST] 🔄 Starting _performPost for orderId: $orderId');
|
|
|
|
Map<String, dynamic>? base = _deliveryBasePayload[orderId];
|
|
|
|
if (base == null) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] Base is null, loading from SharedPreferences');
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if (!mounted) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after prefs load');
|
|
return;
|
|
}
|
|
|
|
base = {
|
|
'logid': 0,
|
|
'tenantid': int.tryParse(prefs.getString('deliverylog_${orderId}_tenantid') ?? '0') ?? 0,
|
|
'partnerid': int.tryParse(prefs.getString('deliverylog_${orderId}_partnerid') ?? '0') ?? 0,
|
|
'locationid': int.tryParse(prefs.getString('deliverylog_${orderId}_locationid') ?? '0') ?? 0,
|
|
'orderheaderid': int.tryParse(prefs.getString('deliverylog_${orderId}_orderheaderid') ?? '0') ?? 0,
|
|
'deliveryid': int.tryParse(prefs.getString('deliverylog_${orderId}_deliveryid') ?? '0') ?? 0,
|
|
'userid': int.tryParse(prefs.getString('deliverylog_${orderId}_userid') ?? '0') ?? 0,
|
|
'orderid': prefs.getString('deliverylog_${orderId}_orderid') ?? orderId,
|
|
'orderstatus': prefs.getString('deliverylog_${orderId}_orderstatus') ?? 'active',
|
|
'starttime': prefs.getString('deliverylog_${orderId}_starttime') ?? '', // Load starttime
|
|
};
|
|
debugPrint('[CART][DELIVERYLOG][POST] Base loaded from prefs: $base');
|
|
} catch (e) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] Error loading base: $e');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// At this point, base is guaranteed to be non-null (either from cache or created above)
|
|
final basePayload = base; // Flow analysis ensures base is non-null here
|
|
|
|
debugPrint('[CART][DELIVERYLOG][POST] Getting coordinates...');
|
|
|
|
// CRITICAL: Get coordinates with retry logic - NEVER post with null or '0' coordinates
|
|
final coords = await _getValidCoordinates().timeout(
|
|
const Duration(seconds: 10), // Increased timeout to allow retries
|
|
onTimeout: () {
|
|
debugPrint('[CART][DELIVERYLOG][POST] ❌ Coordinate timeout after retries');
|
|
return null;
|
|
},
|
|
);
|
|
|
|
if (!mounted) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after coords');
|
|
return;
|
|
}
|
|
|
|
// CRITICAL: Validate coordinates - NEVER post with null, '0', or invalid coordinates
|
|
if (coords == null || coords.$1.isEmpty || coords.$2.isEmpty ||
|
|
coords.$1 == '0' || coords.$2 == '0') {
|
|
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinates (lat=${coords?.$1 ?? 'null'}, lng=${coords?.$2 ?? 'null'})');
|
|
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
|
|
return; // Skip this post - don't send invalid coordinates
|
|
}
|
|
|
|
// Validate coordinate ranges
|
|
final latDouble = double.tryParse(coords.$1) ?? 0.0;
|
|
final lngDouble = double.tryParse(coords.$2) ?? 0.0;
|
|
if (latDouble == 0 || lngDouble == 0 ||
|
|
latDouble.abs() > 90 || lngDouble.abs() > 180) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinate ranges (lat=$latDouble, lng=$lngDouble)');
|
|
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
|
|
return; // Skip this post - don't send invalid coordinates
|
|
}
|
|
|
|
debugPrint('[CART][DELIVERYLOG][POST] ✅ Valid coordinates: lat=${coords.$1}, lng=${coords.$2}');
|
|
|
|
// Cumulative KM is tracked exclusively by LiveTrackingService (high-frequency, every 3s).
|
|
// Do not accumulate here to avoid race conditions with concurrent writers.
|
|
|
|
final now = DateTime.now();
|
|
final logdate = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
|
|
|
// CRITICAL: Ensure starttime is always included in payload
|
|
final starttimeValue = basePayload['starttime']?.toString() ?? '';
|
|
if (starttimeValue.isEmpty) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: starttime is empty in basePayload, using fallback');
|
|
}
|
|
|
|
// CRITICAL: Use validated coordinates - guaranteed to be non-null and valid at this point
|
|
final payload = {
|
|
...basePayload,
|
|
'logdate': logdate,
|
|
'latitude': coords.$1, // Guaranteed non-null and valid
|
|
'longitude': coords.$2, // Guaranteed non-null and valid
|
|
'starttime': starttimeValue.isNotEmpty ? starttimeValue : '', // CRITICAL: Always include starttime
|
|
};
|
|
|
|
// Validate payload has all required fields
|
|
final requiredFields = ['tenantid', 'partnerid', 'locationid', 'orderheaderid', 'deliveryid', 'userid', 'orderid', 'orderstatus', 'starttime'];
|
|
final missingFields = requiredFields.where((field) => payload[field] == null || payload[field] == '').toList();
|
|
if (missingFields.isNotEmpty) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: Missing fields in payload: $missingFields');
|
|
}
|
|
|
|
final url = ApiConstants.mainRoute == 'live'
|
|
? ApiConstants.createDeliveryLogLive
|
|
: ApiConstants.createDeliveryLogDev;
|
|
|
|
debugPrint('[CART][DELIVERYLOG][POST] 📤 Sending to API: $url');
|
|
debugPrint('[CART][DELIVERYLOG][POST] 📦 Payload: $payload');
|
|
debugPrint('[CART][DELIVERYLOG][POST] ✅ starttime in payload: "${payload['starttime']}"');
|
|
|
|
await _deliveryLogProvider
|
|
.createDeliveryLog(url, payload)
|
|
.timeout(
|
|
const Duration(seconds: 8),
|
|
onTimeout: () {
|
|
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ API timeout for orderId: $orderId');
|
|
throw TimeoutException('API timeout', const Duration(seconds: 8));
|
|
},
|
|
);
|
|
|
|
debugPrint('[CART][DELIVERYLOG][POST] ✅ SUCCESS for orderId: $orderId at ${DateTime.now()}');
|
|
} catch (e, stackTrace) {
|
|
debugPrint('[CART][DELIVERYLOG][POST] ❌ ERROR for orderId: $orderId - $e');
|
|
debugPrint('[CART][DELIVERYLOG][POST] Stack trace: $stackTrace');
|
|
}
|
|
}
|
|
|
|
Future<(String lat, String lng)?> _getValidCoordinates({int retryCount = 0}) async {
|
|
const maxRetries = 3;
|
|
|
|
try {
|
|
final bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
|
if (!serviceEnabled) {
|
|
debugPrint('[CART][COORDS] Location service disabled, trying last known position');
|
|
final lastPos = await Geolocator.getLastKnownPosition();
|
|
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
|
|
final lat = lastPos.latitude.toString();
|
|
final lng = lastPos.longitude.toString();
|
|
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
|
|
return (lat, lng);
|
|
}
|
|
// Retry if we haven't exceeded max retries
|
|
if (retryCount < maxRetries) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
return _getValidCoordinates(retryCount: retryCount + 1);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
LocationPermission permission = await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
permission = await Geolocator.requestPermission();
|
|
}
|
|
|
|
if (permission == LocationPermission.deniedForever ||
|
|
permission == LocationPermission.denied) {
|
|
debugPrint('[CART][COORDS] Permission denied, trying last known position');
|
|
final lastPos = await Geolocator.getLastKnownPosition();
|
|
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
|
|
final lat = lastPos.latitude.toString();
|
|
final lng = lastPos.longitude.toString();
|
|
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
|
|
return (lat, lng);
|
|
}
|
|
// Retry if we haven't exceeded max retries
|
|
if (retryCount < maxRetries) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
return _getValidCoordinates(retryCount: retryCount + 1);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Position? position;
|
|
try {
|
|
// Try to get current position with higher accuracy
|
|
position = await Geolocator.getCurrentPosition(
|
|
locationSettings: const LocationSettings(
|
|
accuracy: LocationAccuracy.high, // Changed to high for better accuracy
|
|
timeLimit: Duration(seconds: 8), // Increased timeout
|
|
),
|
|
).timeout(const Duration(seconds: 8));
|
|
} catch (e) {
|
|
debugPrint('[CART][COORDS] Timeout getting current position: $e, trying last known');
|
|
position = await Geolocator.getLastKnownPosition();
|
|
}
|
|
|
|
if (position != null && position.latitude != 0 && position.longitude != 0) {
|
|
final lat = position.latitude.toString();
|
|
final lng = position.longitude.toString();
|
|
|
|
// Validate coordinates are within valid GPS ranges
|
|
final latDouble = double.tryParse(lat) ?? 0.0;
|
|
final lngDouble = double.tryParse(lng) ?? 0.0;
|
|
if (latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('last_lat', lat);
|
|
await prefs.setString('last_lng', lng);
|
|
} catch (_) {}
|
|
debugPrint('[CART][COORDS] ✅ Got valid coordinates: $lat, $lng');
|
|
return (lat, lng);
|
|
} else {
|
|
debugPrint('[CART][COORDS] ⚠️ Invalid coordinate ranges: $lat, $lng');
|
|
}
|
|
}
|
|
|
|
// Fallback to SharedPreferences cached coordinates
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final lat = (prefs.getString('last_lat') ?? '').trim();
|
|
final lng = (prefs.getString('last_lng') ?? '').trim();
|
|
if (lat.isNotEmpty && lng.isNotEmpty && lat != '0' && lng != '0') {
|
|
final latDouble = double.tryParse(lat) ?? 0.0;
|
|
final lngDouble = double.tryParse(lng) ?? 0.0;
|
|
if (latDouble != 0 && lngDouble != 0 && latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
|
|
debugPrint('[CART][COORDS] ✅ Using cached coordinates: $lat, $lng');
|
|
return (lat, lng);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
|
|
// Retry if we haven't exceeded max retries
|
|
if (retryCount < maxRetries) {
|
|
debugPrint('[CART][COORDS] ⚠️ Retry ${retryCount + 1}/$maxRetries to get coordinates');
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
return _getValidCoordinates(retryCount: retryCount + 1);
|
|
}
|
|
|
|
debugPrint('[CART][COORDS] ❌ Failed to get valid coordinates after $maxRetries retries');
|
|
return null;
|
|
} catch (e) {
|
|
debugPrint('[CART][COORDS] ❌ Error getting coordinates: $e');
|
|
// Retry if we haven't exceeded max retries
|
|
if (retryCount < maxRetries) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
return _getValidCoordinates(retryCount: retryCount + 1);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void _stopDeliveryPosting(String orderId) {
|
|
_deliveryTimers[orderId]?.cancel();
|
|
_deliveryTimers.remove(orderId);
|
|
_deliveryBasePayload.remove(orderId);
|
|
debugPrint('[CART][DELIVERYLOG] Stopped timer for orderId: $orderId');
|
|
}
|
|
|
|
void _stopAllTimers() {
|
|
for (final timer in _deliveryTimers.values) {
|
|
timer.cancel();
|
|
}
|
|
_deliveryTimers.clear();
|
|
_deliveryBasePayload.clear();
|
|
debugPrint('[CART][DELIVERYLOG] Stopped all timers');
|
|
}
|
|
|
|
// Method to stop delivery posting for a specific order (called when order is completed)
|
|
void stopDeliveryPostingForOrder(String orderId) {
|
|
_stopDeliveryPosting(orderId);
|
|
// Refresh the list to remove completed orders
|
|
if (mounted) {
|
|
_fetchActive();
|
|
}
|
|
}
|
|
|
|
double _parseD(dynamic v) {
|
|
if (v == null) return 0.0;
|
|
if (v is num) return v.toDouble();
|
|
return double.tryParse(v.toString()) ?? 0.0;
|
|
}
|
|
|
|
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
|
const double R = 6371.0;
|
|
final double dLat = _toRadians(lat2 - lat1);
|
|
final double dLon = _toRadians(lon2 - lon1);
|
|
final double a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
|
math.cos(_toRadians(lat1)) *
|
|
math.cos(_toRadians(lat2)) *
|
|
math.sin(dLon / 2) *
|
|
math.sin(dLon / 2);
|
|
final double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
|
return R * c;
|
|
}
|
|
|
|
double _toRadians(double degrees) {
|
|
return degrees * math.pi / 180.0;
|
|
}
|
|
|
|
String _distanceKmDisplay(Map<String, dynamic> m) {
|
|
// Try API distance first
|
|
final String apiKmsStr = (m['actualkms'] ?? m['distance'] ?? '0').toString();
|
|
final double apiKms = double.tryParse(apiKmsStr) ?? 0.0;
|
|
if (apiKms > 0) {
|
|
return apiKms < 10 ? apiKmsStr : apiKms.toStringAsFixed(0);
|
|
}
|
|
|
|
// Try rider location to delivery location
|
|
final double rLat = _parseD(m['riderslat']);
|
|
final double rLon = _parseD(m['riderslon']);
|
|
final double dLat = _parseD(m['droplat'] ?? m['deliverylat']);
|
|
final double dLon = _parseD(m['droplon'] ?? m['deliverylong']);
|
|
|
|
if (rLat != 0 && rLon != 0 && dLat != 0 && dLon != 0) {
|
|
final double km = _haversineKm(rLat, rLon, dLat, dLon);
|
|
return km.toStringAsFixed(km < 10 ? 1 : 0);
|
|
}
|
|
|
|
// Fallback: pickup to delivery
|
|
final double pLat = _parseD(m['pickuplat']);
|
|
final double pLon = _parseD(m['pickuplon']);
|
|
if (pLat != 0 && pLon != 0 && dLat != 0 && dLon != 0) {
|
|
final double km = _haversineKm(pLat, pLon, dLat, dLon);
|
|
return km.toStringAsFixed(km < 10 ? 1 : 0);
|
|
}
|
|
|
|
return '0';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.grey.shade200,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.grey.shade200,
|
|
elevation: 0,
|
|
centerTitle: false,
|
|
toolbarHeight: 70,
|
|
title: Padding(
|
|
padding: const EdgeInsets.only(top: 12),
|
|
child: Text(
|
|
"Active Deliveries",
|
|
style: TextStyle(
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.bold,
|
|
fontFamily: FontConstants.fontFamily,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
),
|
|
bottom: const PreferredSize(
|
|
preferredSize: Size.fromHeight(1),
|
|
child: Divider(height: 1, color: Colors.grey),
|
|
),
|
|
),
|
|
body: SafeArea(
|
|
child: _activeDeliveries.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const SizedBox(height: 80),
|
|
Transform.translate(
|
|
offset: const Offset(0, -11),
|
|
child: Image.asset(
|
|
"assets/images/Nearle Bike.png",
|
|
errorBuilder: (c, e, s) => const Icon(
|
|
Icons.delivery_dining,
|
|
size: 120,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Transform.translate(
|
|
offset: const Offset(0, -11),
|
|
child: Text(
|
|
"No Active Deliveries",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontFamily: FontConstants.fontFamily,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: RefreshIndicator(
|
|
onRefresh: _fetchActive,
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
itemCount: _activeDeliveries.length,
|
|
itemBuilder: (context, index) {
|
|
final item = _activeDeliveries[index];
|
|
final customerName = (item['deliverycustomer'] ?? item['customer'] ?? 'Customer').toString();
|
|
final address = (item['deliveryaddress'] ?? item['address'] ?? 'Address not available').toString();
|
|
final storeName = (item['pickupcustomer'] ?? item['store'] ?? 'Store').toString();
|
|
final orderId = (item['orderid'] ?? '').toString();
|
|
final distance = _distanceKmDisplay(item);
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// -------------------------------------------
|
|
// TOP CUSTOMER DETAILS
|
|
// -------------------------------------------
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Column(
|
|
children: [
|
|
Container(
|
|
width: 12,
|
|
height: 12,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.orange,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
Container(
|
|
width: 2,
|
|
height: 30,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
Container(
|
|
width: 12,
|
|
height: 12,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.green,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Transform.translate(
|
|
offset: const Offset(0, -3),
|
|
child: Text(
|
|
customerName,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 18,
|
|
color: Colors.black,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Transform.translate(
|
|
offset: const Offset(0, 8),
|
|
child: Text(
|
|
address,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.black87,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Distance: $distance km',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.blueGrey,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () async {
|
|
// Just launch dialer; PiP is handled only from navigation screen
|
|
final phone =
|
|
(item['deliverycontactno'] ?? '').toString();
|
|
final bool success = await launchPhoneDialer(
|
|
phone.isNotEmpty ? phone : '9876543210',
|
|
);
|
|
if (!success && context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Could not launch dialer'),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: Image.asset(
|
|
'assets/images/phone-call .png',
|
|
height: 27,
|
|
width: 27,
|
|
errorBuilder: (c, e, s) =>
|
|
const Icon(Icons.phone, size: 27, color: Colors.green),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Divider(),
|
|
const SizedBox(height: 8),
|
|
// -------------------------------------------
|
|
// STORE DETAILS
|
|
// -------------------------------------------
|
|
Row(
|
|
children: [
|
|
Image.asset(
|
|
'assets/images/shoppingbag.png',
|
|
height: 32,
|
|
width: 32,
|
|
errorBuilder: (c, e, s) => const Icon(
|
|
Icons.shopping_bag,
|
|
size: 32,
|
|
color: Colors.orange,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
storeName,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 18,
|
|
color: Colors.black,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () async {
|
|
_showMyOptionsSheet(
|
|
context,
|
|
item,
|
|
null, // No parent state for cart page
|
|
);
|
|
// Refresh cart page after skip (order will no longer be active)
|
|
if (mounted) {
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
_fetchActive();
|
|
}
|
|
},
|
|
child: Transform.translate(
|
|
offset: const Offset(0, -5),
|
|
child: Text(
|
|
'Skip>>',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
fontFamily: FontConstants.fontFamily,
|
|
color: ColorConstants.primaryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Text(
|
|
"Order ID: #$orderId",
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.black54,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 15),
|
|
// -------------------------------------------
|
|
// SLIDER BUTTON
|
|
// -------------------------------------------
|
|
SliderButton(
|
|
properties: SliderButtonProperties(
|
|
height: 50,
|
|
buttonSize: 45,
|
|
width: MediaQuery.of(context).size.width - 56,
|
|
backgroundColor: ColorConstants.primaryColor,
|
|
dismissThresholds: 0.90,
|
|
action: () async {
|
|
await Future.delayed(const Duration(milliseconds: 400));
|
|
if (!context.mounted) return false;
|
|
|
|
// Navigate to delivery map screen (same as deliveries page)
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => _DeliveryMapScreen(
|
|
delivery: item,
|
|
parentState: null, // No parent state for cart page
|
|
),
|
|
),
|
|
);
|
|
|
|
// Refresh cart page when returning from map screen
|
|
// (in case delivery was completed/cancelled)
|
|
if (mounted) {
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
_fetchActive();
|
|
}
|
|
|
|
return false;
|
|
},
|
|
label: const Text(
|
|
'Slide to start Delivery',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
icon: ClipOval(
|
|
child: Material(
|
|
color: Colors.white,
|
|
child: SizedBox(
|
|
width: 45,
|
|
height: 45,
|
|
child: Center(
|
|
child: Text(
|
|
'${index + 1}',
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|