Files
doormile_milderapp/lib/xpress/views/Dashboard/deliveries/nav.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 05:40:35 +05:30

1040 lines
36 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
part of 'deliveries.dart';
// -------------------------------------------------------------------------
// SCREEN 2: RIDER NAVIGATION (Live tracking + auto-opens Google Maps once)
// -------------------------------------------------------------------------
class _RiderNavigationScreen extends StatefulWidget {
final Map<String, dynamic> delivery;
final _MyDeliveriesState? parentState;
const _RiderNavigationScreen({
// ignore: unused_element_parameter
super.key,
required this.delivery,
this.parentState,
});
@override
State<_RiderNavigationScreen> createState() => _RiderNavigationScreenState();
}
class _RiderNavigationScreenState extends State<_RiderNavigationScreen>
with WidgetsBindingObserver {
DoormileMapController? mapController;
bool _mapReady = false;
bool _isDisposed = false;
Position? _currentPosition;
StreamSubscription<Position>? _positionStream;
List<DoormileMapMarker> _markers = const [];
MarkerIcon? _riderIcon;
MarkerIcon? _dropIcon;
LatLng? _lastRiderPosition;
late final LatLng dropLocation;
bool _isInitializing = true;
bool _hasOpenedGoogleMaps = false;
Timer? _autoOpenTimer;
Timer? _pipLoaderTimer;
bool _showPipExpandLoader = false;
bool _wasInPipBeforePause = false;
// ETA timer for full navigation screen (top-right corner)
final CountDownController _navEtaController = CountDownController();
int _navEtaRemainingSeconds = 0;
final DeliveriesController deliveriesController = Get.put(
DeliveriesController(),
permanent: true,
);
final Floating floating = Floating();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_resolveDropFromDelivery();
_loadMarkerIcons();
_initializeMap();
_initEtaForNav();
WidgetsBinding.instance.addPostFrameCallback((_) {
_attemptAutoOpenNavigation();
});
_autoOpenTimer = Timer(
const Duration(seconds: 3),
() => _attemptAutoOpenNavigation(fromTimer: true),
);
}
// Initialize remaining ETA for navigation screen using same persisted end time
Future<void> _initEtaForNav() async {
try {
final prefs = await SharedPreferences.getInstance();
final orderId =
widget.delivery['orderid']?.toString() ??
widget.delivery['OrderId']?.toString() ??
'';
if (orderId.isEmpty) return;
final endKey = 'eta_endtime_$orderId';
final endSeconds = prefs.getInt(endKey);
final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000;
int remaining = 0;
// ✅ FIX: Checking (endSeconds > nowSeconds) caused the timer to RESET if user was late.
// We must trust existing endSeconds even if it's in the past.
if (endSeconds != null && endSeconds > 0) {
remaining = endSeconds - nowSeconds;
debugPrint(
'[NAV] Loaded saved ETA end time: $endSeconds (Remaining: $remaining)',
);
} else {
// Only start a NEW timer if one doesn't exist
final rawEta = widget.delivery['eta'];
int etaMinutes = 0;
if (rawEta != null) {
etaMinutes = int.tryParse(rawEta.toString()) ?? 0;
}
if (etaMinutes > 0) {
remaining = etaMinutes * 60;
final newEndSeconds = nowSeconds + remaining;
await prefs.setInt(endKey, newEndSeconds);
debugPrint(
'[NAV] Saved NEW ETA end time: $newEndSeconds (Remaining: $remaining)',
);
}
}
if (!mounted) return;
setState(() {
_navEtaRemainingSeconds = remaining.clamp(0, 24 * 60 * 60);
});
} catch (_) {
// Ignore errors – timer simply won't show
}
}
@override
void dispose() {
_isDisposed = true;
WidgetsBinding.instance.removeObserver(this);
_autoOpenTimer?.cancel();
_pipLoaderTimer?.cancel();
_positionStream?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState lifecycleState) {
if (!mounted) return;
if (lifecycleState == AppLifecycleState.paused ||
lifecycleState == AppLifecycleState.inactive) {
final wasInPip = deliveriesController.isPipEnabled.value;
_wasInPipBeforePause = wasInPip;
if (wasInPip) {
_pipLoaderTimer?.cancel();
if (mounted) {
setState(() {
_showPipExpandLoader = true;
});
}
}
} else if (lifecycleState == AppLifecycleState.resumed) {
// When app comes back to foreground, consider PiP session ended.
deliveriesController.isPipEnabled.value = false;
// ✅ FIX: Refresh the timer when coming back from PiP/Background
_initEtaForNav();
if (_wasInPipBeforePause) {
_wasInPipBeforePause = false;
_pipLoaderTimer?.cancel();
_pipLoaderTimer = Timer(const Duration(milliseconds: 900), () {
if (!mounted) return;
setState(() {
_showPipExpandLoader = false;
});
});
}
}
}
Future<void> enablePip(
BuildContext context, {
bool autoEnable = false,
}) async {
if (!mounted) return;
debugPrint('[NAV] Enabling PiP for navigation screen');
const rational = Rational.landscape();
final screenSize =
MediaQuery.of(context).size * MediaQuery.of(context).devicePixelRatio;
final height = (screenSize.height * 0.5).toInt();
final width = (screenSize.width * 0.9).toInt();
final arguments = autoEnable
? OnLeavePiP(
aspectRatio: rational,
sourceRectHint: math.Rectangle<int>(
0,
(screenSize.height ~/ 1) - (height ~/ 1),
screenSize.width.toInt(),
height,
),
)
: ImmediatePiP(
aspectRatio: rational,
sourceRectHint: math.Rectangle<int>(
((screenSize.width - width) ~/ 2).toInt(),
((screenSize.height - height) ~/ 2).toInt(),
width,
height,
),
);
final status = await floating.enable(arguments);
deliveriesController.isPipEnabled.value = true;
debugPrint('PiP enabled? $status');
}
double _parseD(dynamic v) {
if (v == null) return 0.0;
if (v is num) return v.toDouble();
return double.tryParse(v.toString()) ?? 0.0;
}
void _resolveDropFromDelivery() {
final d = widget.delivery;
final double dropLat = _parseD(
d['droplat'] ?? d['DropLat'] ?? d['deliverylat'],
);
final double dropLon = _parseD(
d['droplon'] ?? d['DropLon'] ?? d['deliverylong'],
);
dropLocation = (dropLat != 0 && dropLon != 0)
? LatLng(dropLat, dropLon)
: const LatLng(11.018356, 77.012596);
}
Future<void> _initializeMap() async {
try {
if (mounted) {
setState(() {
Geolocator.getLastKnownPosition().then((lastPos) {
if (lastPos != null && mounted) {
setState(() {
_currentPosition = lastPos;
_addMarkers(LatLng(lastPos.latitude, lastPos.longitude));
});
_startLiveTracking();
}
});
if (_currentPosition == null) {
_currentPosition = Position(
latitude: dropLocation.latitude,
longitude: dropLocation.longitude,
timestamp: DateTime.now(),
accuracy: 0,
altitude: 0,
heading: 0,
speed: 0,
speedAccuracy: 0,
altitudeAccuracy: 0,
headingAccuracy: 0,
);
_addMarkers(dropLocation);
}
_isInitializing = false;
});
}
_getCurrentLocation().then((_) {
_startLiveTracking();
});
} catch (e) {
debugPrint('[NAVIGATION] Error initializing map: $e');
if (mounted) {
setState(() {
if (_currentPosition == null) {
_currentPosition = Position(
latitude: dropLocation.latitude,
longitude: dropLocation.longitude,
timestamp: DateTime.now(),
accuracy: 0,
altitude: 0,
heading: 0,
speed: 0,
speedAccuracy: 0,
altitudeAccuracy: 0,
headingAccuracy: 0,
);
_addMarkers(dropLocation);
}
_isInitializing = false;
});
}
}
}
Future<void> _getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && mounted) {
setState(() {
_currentPosition = lastPos;
_isInitializing = false;
});
}
Geolocator.openLocationSettings();
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever ||
permission == LocationPermission.denied) {
final lastPos = await Geolocator.getLastKnownPosition();
if (lastPos != null && mounted) {
setState(() {
_currentPosition = lastPos;
_isInitializing = false;
});
_addMarkers(LatLng(lastPos.latitude, lastPos.longitude));
} else if (mounted) {
setState(() => _isInitializing = false);
}
return;
}
Position? pos;
try {
pos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: const Duration(seconds: 5),
).timeout(const Duration(seconds: 5));
} catch (e) {
debugPrint('[NAVIGATION] Timeout getting position, using last known: $e');
pos = await Geolocator.getLastKnownPosition();
}
if (pos != null && mounted) {
setState(() {
_currentPosition = pos;
_isInitializing = false;
});
_addMarkers(LatLng(pos.latitude, pos.longitude));
} else if (mounted) {
setState(() => _isInitializing = false);
}
}
void _startLiveTracking() {
_positionStream =
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.bestForNavigation,
distanceFilter: 5,
),
).listen((Position position) {
if (mounted) {
setState(() {
_currentPosition = position;
_addMarkers(LatLng(position.latitude, position.longitude));
});
final distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
dropLocation.latitude,
dropLocation.longitude,
);
deliveriesController.deliverableDistance.value = distance;
_safeAnimateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(position.latitude, position.longitude),
zoom: 16.5,
tilt: 45,
),
),
);
}
});
}
/// Marker bitmaps are drawn on a canvas, so they arrive asynchronously.
/// Once they land, rebuild whatever markers are already pending.
Future<void> _loadMarkerIcons() async {
final rider = await MapMarkerIcons.dot(Colors.blueAccent);
final drop = await MapMarkerIcons.pin(Colors.red);
if (!mounted || _isDisposed) return;
setState(() {
_riderIcon = rider;
_dropIcon = drop;
_addMarkers(_lastRiderPosition ?? dropLocation);
});
}
/// Mutates [_markers] only — callers own the [setState] so this can be
/// invoked from inside an existing one without nesting.
void _addMarkers(LatLng riderPos) {
_lastRiderPosition = riderPos;
final riderIcon = _riderIcon;
final dropIcon = _dropIcon;
if (riderIcon == null || dropIcon == null) return;
_markers = [
DoormileMapMarker(
id: 'rider',
position: riderPos,
icon: riderIcon,
anchor: MarkerAnchor.center,
),
DoormileMapMarker(id: 'drop', position: dropLocation, icon: dropIcon),
];
}
Future<void> _attemptAutoOpenNavigation({bool fromTimer = false}) async {
if (!mounted || _hasOpenedGoogleMaps) return;
final bool opened = await _openGoogleMapsNavigation();
if (!opened && fromTimer) {
debugPrint('[NAVIGATION] Auto open failed, waiting for manual trigger');
}
}
Future<bool> _openGoogleMapsNavigation() async {
if (!mounted) return false;
if (_hasOpenedGoogleMaps) return true;
try {
if (Platform.isAndroid) {
try {
// ✅ CRITICAL: ENFORCE PiP when starting navigation (compulsory for active deliveries)
final dc = Get.put(DeliveriesController(), permanent: true);
// Enable PiP using floating package (primary)
if (!dc.isPipEnabled.value) {
await enablePip(context);
// Wait a moment for PiP to activate
await Future.delayed(const Duration(milliseconds: 300));
// Also tell native side (if available) – best-effort
const channel = MethodChannel('doormile/pip');
try {
await channel.invokeMethod<bool>('enterPip');
} catch (_) {}
dc.isPipEnabled.value = true;
}
} on PlatformException catch (e) {
debugPrint('[NAV] Method channel PiP failed: ${e.message}');
} catch (e) {
debugPrint('[NAV] Error enabling PiP: $e');
}
}
Position? currentPos;
try {
currentPos = await Geolocator.getLastKnownPosition();
} catch (_) {}
if (currentPos == null) {
try {
currentPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: const Duration(seconds: 2),
);
} catch (_) {}
}
final originLat = currentPos?.latitude ?? dropLocation.latitude;
final originLng = currentPos?.longitude ?? dropLocation.longitude;
final destLat = dropLocation.latitude;
final destLng = dropLocation.longitude;
final Uri nativeUri = Uri.parse(
'google.navigation:q=$destLat,$destLng&mode=d',
);
final Uri webUri = Uri.parse(
'https://www.google.com/maps/dir/?api=1'
'&origin=$originLat,$originLng'
'&destination=$destLat,$destLng'
'&travelmode=driving'
'&dir_action=navigate',
);
// Try native intent first without relying on canLaunch (can be flaky)
bool launched = false;
try {
launched = await launchUrl(
nativeUri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
debugPrint('[NAVIGATION] Native intent failed: $e');
}
if (!launched) {
try {
launched = await launchUrl(
webUri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
debugPrint('[NAVIGATION] Web intent failed: $e');
}
}
if (launched && mounted) {
setState(() {
_hasOpenedGoogleMaps = true;
});
debugPrint('[NAVIGATION] Opened Google Maps');
return true;
}
debugPrint('[NAVIGATION] Could not open Google Maps');
} catch (e) {
debugPrint('[NAVIGATION] Error opening Google Maps: $e');
}
return false;
}
@override
Widget build(BuildContext context) {
return Obx(() {
if (deliveriesController.isPipEnabled.value) {
final rawEta = widget.delivery['eta'];
int etaMinutes = 0;
if (rawEta != null) {
etaMinutes = int.tryParse(rawEta.toString()) ?? 0;
}
return PipInfoCard(
orderId:
widget.delivery['orderid']?.toString() ??
widget.delivery['OrderId']?.toString() ??
'N/A',
etaMinutes: etaMinutes,
);
}
return Scaffold(
body: Stack(
children: [
DoormileMap(
onMapReady: (controller) {
mapController = controller;
_mapReady = true;
},
initialCameraPosition: CameraPosition(
target: LatLng(
_currentPosition?.latitude ?? dropLocation.latitude,
_currentPosition?.longitude ?? dropLocation.longitude,
),
zoom: 15,
tilt: 45,
),
markers: _markers,
myLocationEnabled: true,
compassEnabled: true,
tiltGesturesEnabled: true,
),
if (!_hasOpenedGoogleMaps)
Positioned(
top: 50,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
decoration: BoxDecoration(
color: ColorConstants.primaryColor,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
const SizedBox(width: 12),
Text(
'Opening Google Maps...',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
),
),
Positioned(
top: _hasOpenedGoogleMaps ? 50 : 120,
left: 16,
child: CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () {
_autoOpenTimer?.cancel();
// Navigate back to BottomPage on Deliveries tab
Get.offAll(() => const BottomPage(initialIndex: 1));
},
),
),
),
if (_hasOpenedGoogleMaps)
Positioned(
top: 50,
right: 16,
child: CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.navigation, color: Colors.blue),
onPressed: () async {
await _openGoogleMapsNavigation();
},
tooltip: 'Open Google Maps',
),
),
),
// Small real-time ETA timer on top-right in full navigation (not in PiP)
if (!deliveriesController.isPipEnabled.value &&
_navEtaRemainingSeconds > 0)
Positioned(
top: 110,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 32,
height: 32,
child: CircularCountDownTimer(
key: ValueKey(
'nav_timer_$_navEtaRemainingSeconds',
), // Force rebuild if duration changes significantly
duration: _navEtaRemainingSeconds,
initialDuration: 0,
controller: _navEtaController,
width: 32,
height: 32,
ringColor: ColorConstants.primaryColor.withValues(
alpha: 0.15,
),
fillColor: ColorConstants.primaryColor,
backgroundColor: Colors.white,
strokeWidth: 3,
strokeCap: StrokeCap.round,
textStyle: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: ColorConstants.primaryColor,
fontFamily: FontConstants.fontFamily,
),
isReverse: true,
isReverseAnimation: true,
isTimerTextShown: true,
autoStart: true,
timeFormatterFunction: (defaultFormatter, duration) {
return duration.inSeconds <= 0
? 'Out'
: defaultFormatter(duration);
},
),
),
const SizedBox(width: 6),
Text(
'ETA',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: Colors.black87,
),
),
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Customer Details',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
InkWell(
onTap: () async {
// Enable PiP mode first before launching dialer
if (!deliveriesController.isPipEnabled.value) {
await enablePip(context);
// Wait a moment for PiP to activate
await Future.delayed(
const Duration(milliseconds: 300),
);
}
final phone =
(widget.delivery['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: 10),
const Divider(thickness: 1.5),
const SizedBox(height: 10),
_buildCustomerInfo(),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () async {
// Optimize: Run PiP enable and Prefs saving in parallel
final d = widget.delivery;
final double? lat = double.tryParse(
"${d['droplat']}",
);
final double? lng = double.tryParse(
"${d['droplon']}",
);
if (lat == null || lng == null) {
debugPrint("[RIDE] Invalid drop location");
return;
}
final prefsFuture =
SharedPreferences.getInstance().then((
prefs,
) async {
await prefs.setString(
"drop_lat",
lat.toString(),
);
await prefs.setString(
"drop_lng",
lng.toString(),
);
});
// PiP Logic
if (!deliveriesController.isPipEnabled.value) {
try {
debugPrint(
'[NAV] Enforcing PiP before Ride button',
);
// Don't await extremely long, just enough to start transition
await enablePip(context);
await Future.delayed(
const Duration(milliseconds: 150),
);
} catch (e) {
debugPrint('[NAV] Error enabling PiP: $e');
}
}
// Construct URL (Map uses current location by default if origin not specified)
final String url =
"https://www.google.com/maps/dir/?api=1&destination=$lat,$lng&travelmode=driving";
final Uri uri = Uri.parse(url);
// Fire and forget prefs
prefsFuture.ignore();
try {
// Direct launch is faster and avoids package visibility queries
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
debugPrint("[RIDE] Opening Google Maps...");
} catch (e) {
debugPrint(
"[RIDE] Could not launch Maps: $e",
);
}
},
child: Text(
'Ride',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
SizedBox(width: 12), // space between buttons
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
minimumSize: const Size(
0,
48,
), // FIX: no infinity width
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () async {
final result = await showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (context) => _DeliveryBottomSheet(
delivery: widget.delivery,
parentState: widget.parentState,
),
);
if (result == true) {
// ✅ If delivery updated/skipped, close navigation screen immediately
if (mounted && Navigator.canPop(context)) {
Navigator.pop(context);
}
}
},
child: Text(
'Update',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
],
),
),
),
),
if (_showPipExpandLoader)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.45),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 56,
height: 56,
child: CircularProgressIndicator(
strokeWidth: 4,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
const SizedBox(height: 16),
Text(
'Restoring full view...',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
),
],
),
);
});
}
Widget _buildCustomerInfo() {
final rawCollection = widget.delivery['collectionamt'];
double collectionAmt = 0.0;
if (rawCollection != null) {
if (rawCollection is num) {
collectionAmt = rawCollection.toDouble();
} else {
collectionAmt = double.tryParse(rawCollection.toString()) ?? 0.0;
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
'Name:',
(widget.delivery['deliverycustomer'] ?? 'Customer').toString(),
),
const SizedBox(height: 10),
_buildInfoRow(
'Address:',
(widget.delivery['deliveryaddress'] ?? 'Address not available')
.toString(),
isExpanded: true,
),
const SizedBox(height: 10),
_buildInfoRow(
'Order ID:',
'#${(widget.delivery['orderid'] ?? '').toString()}',
),
if (collectionAmt > 0) ...[
const SizedBox(height: 10),
_buildInfoRow('To Collect:', '₹${collectionAmt.toStringAsFixed(0)}'),
],
],
);
}
Widget _buildInfoRow(String label, String value, {bool isExpanded = false}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(width: 15),
isExpanded
? Expanded(
child: Text(
value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18,
fontFamily: FontConstants.fontFamily,
color: Colors.black87,
),
),
)
: Text(
value,
style: TextStyle(
fontSize: 18,
fontFamily: FontConstants.fontFamily,
color: Colors.black87,
),
),
],
);
}
void _safeAnimateCamera(CameraUpdate update) {
if (!_mapReady || _isDisposed) return;
mapController?.animateCamera(update);
}
}