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>
This commit is contained in:
329
lib/xpress/views/Dashboard/deliveries/card.dart
Normal file
329
lib/xpress/views/Dashboard/deliveries/card.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELIVERY CARD
|
||||
// -------------------------------------------------------------------------
|
||||
class DeliveryCard extends StatelessWidget {
|
||||
final Map<String, dynamic> item;
|
||||
final int displayStep;
|
||||
final String distanceStr;
|
||||
final bool enabled;
|
||||
final bool isSkipped;
|
||||
|
||||
const DeliveryCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.displayStep,
|
||||
required this.distanceStr,
|
||||
this.enabled = true,
|
||||
this.isSkipped = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String customerName = (item['deliverycustomer'] ?? 'Customer')
|
||||
.toString();
|
||||
final String address = (item['deliveryaddress'] ?? 'Address not available')
|
||||
.toString();
|
||||
final String tenantName = (item['tenantname'] ?? 'Store').toString();
|
||||
final String orderId = (item['orderid'] ?? '').toString();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isSkipped ? Border.all(color: Colors.orange, width: 2) : null,
|
||||
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: [
|
||||
if (isSkipped)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'SKIPPED',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSkipped) const SizedBox(height: 8),
|
||||
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.bold,
|
||||
fontSize: 17.5,
|
||||
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: $distanceStr km',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
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),
|
||||
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(
|
||||
tenantName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
if (!isSkipped)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final parentState = context
|
||||
.findAncestorStateOfType<
|
||||
_MyDeliveriesState
|
||||
>();
|
||||
if (parentState != null) {
|
||||
_showMyOptionsSheet(
|
||||
context,
|
||||
item,
|
||||
parentState,
|
||||
);
|
||||
}
|
||||
},
|
||||
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),
|
||||
SliderButton(
|
||||
properties: SliderButtonProperties(
|
||||
height: 50,
|
||||
buttonSize: 45,
|
||||
width: MediaQuery.of(context).size.width - 56,
|
||||
backgroundColor: enabled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade400,
|
||||
dismissThresholds: 0.90,
|
||||
action: enabled
|
||||
? () async {
|
||||
// Reduce delay to make it feel snappier
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (!context.mounted) return false;
|
||||
final parentState = context
|
||||
.findAncestorStateOfType<_MyDeliveriesState>();
|
||||
|
||||
// ✅ BLOCK: Check if there's already an active delivery (and this isn't it)
|
||||
if (parentState != null) {
|
||||
final currentOrderId = (item['orderid'] ?? '')
|
||||
.toString();
|
||||
final activeOrderIds = parentState._activeDeliveries
|
||||
.map((d) => (d['orderid'] ?? '').toString())
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// Block if there's a different active delivery: just ignore the swipe
|
||||
if (parentState._activeDeliveries.isNotEmpty &&
|
||||
!activeOrderIds.contains(currentOrderId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to delivery map screen
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _DeliveryMapScreen(
|
||||
delivery: item,
|
||||
parentState: parentState,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
: () async => null,
|
||||
label: Transform.translate(
|
||||
offset: const Offset(-0.5, 0),
|
||||
child: Text(
|
||||
isSkipped
|
||||
? 'Slide to resume Delivery'
|
||||
: (enabled
|
||||
? 'Slide to start Delivery'
|
||||
: 'Complete previous delivery'),
|
||||
style: const TextStyle(
|
||||
fontSize: 18.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
icon: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$displayStep',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: enabled
|
||||
? const ui.Color.fromARGB(255, 0, 0, 0)
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1761
lib/xpress/views/Dashboard/deliveries/deliveries.dart
Normal file
1761
lib/xpress/views/Dashboard/deliveries/deliveries.dart
Normal file
File diff suppressed because it is too large
Load Diff
284
lib/xpress/views/Dashboard/deliveries/done.dart
Normal file
284
lib/xpress/views/Dashboard/deliveries/done.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELIVERIES DONE SCREEN
|
||||
// -------------------------------------------------------------------------
|
||||
class DeliveriesDone extends StatefulWidget {
|
||||
final bool isCancelled;
|
||||
final int bonusPoints;
|
||||
|
||||
const DeliveriesDone({
|
||||
super.key,
|
||||
this.isCancelled = false,
|
||||
this.bonusPoints = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DeliveriesDone> createState() => _DeliveriesDoneState();
|
||||
}
|
||||
|
||||
class _DeliveriesDoneState extends State<DeliveriesDone> {
|
||||
late ConfettiController _confettiController;
|
||||
final GlobalKey<ScratcherState> _scratcherKey = GlobalKey<ScratcherState>();
|
||||
double _opacity = 0.0;
|
||||
bool _isScratched = false; // Track scratch state
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_confettiController = ConfettiController(
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Check if we should show the scratch card
|
||||
final bool showScratchCard = !widget.isCancelled && widget.bonusPoints > 0;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Main Content
|
||||
SafeArea(
|
||||
child: showScratchCard
|
||||
? _buildScratchCardContent()
|
||||
: _buildStandardContent(),
|
||||
),
|
||||
|
||||
// Confetti Layer (on top)
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
shouldLoop: false,
|
||||
colors: const [
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.pink,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
],
|
||||
createParticlePath: drawStar,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStandardContent() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Lottie.asset(
|
||||
widget.isCancelled
|
||||
? 'assets/lotties/Error Occurred!.json'
|
||||
: 'assets/lotties/result page succes.json',
|
||||
height: 220,
|
||||
repeat: false,
|
||||
errorBuilder: (c, e, s) => Icon(
|
||||
widget.isCancelled ? Icons.cancel : Icons.check_circle,
|
||||
size: 120,
|
||||
color: widget.isCancelled ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
widget.isCancelled ? 'Delivery Cancelled' : 'Delivery Completed!',
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.isCancelled
|
||||
? 'This order was cancelled.'
|
||||
: 'Great job! Your delivery was successful.',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildDoneButton(isEnabled: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScratchCardContent() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Text(
|
||||
'You won a Scratch Card!',
|
||||
style: TextStyle(
|
||||
fontSize: 28, // Increased
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Scratch to reveal your bonus points',
|
||||
style: TextStyle(
|
||||
fontSize: 18, // Increased
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Scratcher(
|
||||
key: _scratcherKey,
|
||||
brushSize: 50,
|
||||
threshold: 50,
|
||||
color: ColorConstants.primaryColor,
|
||||
onChange: (value) {
|
||||
// Optional: haptic feedback or sound while scratching
|
||||
},
|
||||
onThreshold: () {
|
||||
_confettiController.play();
|
||||
setState(() {
|
||||
_opacity = 1.0;
|
||||
_isScratched = true; // Enable button
|
||||
});
|
||||
},
|
||||
// Custom cover content instead of just solid color
|
||||
// Was `doormilelauncher.png`, which is not in this repo — the
|
||||
// same file ships here under the name below, and the scratch
|
||||
// card was rendering an exception instead of its cover.
|
||||
image: Image.asset(
|
||||
'assets/images/doormile_icon.png',
|
||||
fit: BoxFit.scaleDown,
|
||||
width: 100, // Constrain width so it fits nicely
|
||||
height: 100,
|
||||
),
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.monetization_on,
|
||||
size: 80,
|
||||
color: Colors.amber,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'${widget.bonusPoints}',
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Points',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildDoneButton(isEnabled: _isScratched),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDoneButton({required bool isEnabled}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isEnabled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: isEnabled
|
||||
? () {
|
||||
Get.offAll(() => const BottomPage(initialIndex: 1));
|
||||
}
|
||||
: null,
|
||||
child: const Text(
|
||||
'Done',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Path drawStar(Size size) {
|
||||
// Method to draw star shape for confetti
|
||||
double degToRad(double deg) => deg * (math.pi / 180.0);
|
||||
|
||||
const numberOfPoints = 5;
|
||||
final halfWidth = size.width / 2;
|
||||
final externalRadius = halfWidth;
|
||||
final internalRadius = halfWidth / 2.5;
|
||||
final degreesPerStep = degToRad(360 / numberOfPoints);
|
||||
final halfDegreesPerStep = degreesPerStep / 2;
|
||||
final path = Path();
|
||||
final fullAngle = degToRad(360);
|
||||
path.moveTo(size.width, halfWidth);
|
||||
|
||||
for (double step = 0; step < fullAngle; step += degreesPerStep) {
|
||||
path.lineTo(
|
||||
halfWidth + externalRadius * math.cos(step),
|
||||
halfWidth + externalRadius * math.sin(step),
|
||||
);
|
||||
path.lineTo(
|
||||
halfWidth + internalRadius * math.cos(step + halfDegreesPerStep),
|
||||
halfWidth + internalRadius * math.sin(step + halfDegreesPerStep),
|
||||
);
|
||||
}
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
}
|
||||
891
lib/xpress/views/Dashboard/deliveries/map.dart
Normal file
891
lib/xpress/views/Dashboard/deliveries/map.dart
Normal file
@@ -0,0 +1,891 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SCREEN 1: DELIVERY MAP PREVIEW (Shows route, has "Start" button)
|
||||
// -------------------------------------------------------------------------
|
||||
class _DeliveryMapScreen extends StatefulWidget {
|
||||
final Map<String, dynamic> delivery;
|
||||
final _MyDeliveriesState? parentState;
|
||||
const _DeliveryMapScreen({required this.delivery, this.parentState});
|
||||
|
||||
@override
|
||||
State<_DeliveryMapScreen> createState() => _DeliveryMapScreenState();
|
||||
}
|
||||
|
||||
class _DeliveryMapScreenState extends State<_DeliveryMapScreen> {
|
||||
DoormileMapController? _mapController;
|
||||
late final LatLng _pickupLocation;
|
||||
late final LatLng _dropLocation;
|
||||
List<DoormileMapMarker> _markers = const [];
|
||||
List<DoormileMapPolyline> _polylines = const [];
|
||||
bool _isNavigating = false; // Prevent multiple clicks
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_resolveLocationsFromDelivery();
|
||||
_setMarkers();
|
||||
_createRealRoute();
|
||||
}
|
||||
|
||||
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 _resolveLocationsFromDelivery() {
|
||||
final d = widget.delivery;
|
||||
final double pickLat = _parseD(d['pickuplat'] ?? d['PickupLat']);
|
||||
final double pickLon = _parseD(d['pickuplon'] ?? d['PickupLon']);
|
||||
final double dropLat = _parseD(
|
||||
d['droplat'] ?? d['DropLat'] ?? d['deliverylat'],
|
||||
);
|
||||
final double dropLon = _parseD(
|
||||
d['droplon'] ?? d['DropLon'] ?? d['deliverylong'],
|
||||
);
|
||||
final double riderLat = _parseD(d['riderslat']);
|
||||
final double riderLon = _parseD(d['riderslon']);
|
||||
|
||||
final bool hasPickup = pickLat != 0 && pickLon != 0;
|
||||
final bool hasDrop = dropLat != 0 && dropLon != 0;
|
||||
|
||||
final LatLng pickup = hasPickup
|
||||
? LatLng(pickLat, pickLon)
|
||||
: (riderLat != 0 && riderLon != 0
|
||||
? LatLng(riderLat, riderLon)
|
||||
: const LatLng(10.998356, 76.977596));
|
||||
|
||||
final LatLng drop = hasDrop
|
||||
? LatLng(dropLat, dropLon)
|
||||
: const LatLng(11.004556, 76.967696);
|
||||
|
||||
_pickupLocation = pickup;
|
||||
_dropLocation = drop;
|
||||
}
|
||||
|
||||
Future<void> _setMarkers() async {
|
||||
final pickupIcon = await MapMarkerIcons.pin(Colors.red);
|
||||
final dropIcon = await MapMarkerIcons.pin(Colors.green);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_markers = [
|
||||
DoormileMapMarker(
|
||||
id: 'pickup',
|
||||
position: _pickupLocation,
|
||||
icon: pickupIcon,
|
||||
),
|
||||
DoormileMapMarker(id: 'drop', position: _dropLocation, icon: dropIcon),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _createRealRoute() async {
|
||||
final route = await RoutingService.getRoute(
|
||||
origin: _pickupLocation,
|
||||
destination: _dropLocation,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Fall back to a straight line so the screen still shows the connection
|
||||
// when routing is unavailable.
|
||||
final points = (route != null && route.points.isNotEmpty)
|
||||
? route.points
|
||||
: <LatLng>[_pickupLocation, _dropLocation];
|
||||
|
||||
if (route == null) debugPrint('[MAP_PREVIEW] No route found');
|
||||
|
||||
setState(() {
|
||||
_polylines = [
|
||||
DoormileMapPolyline(
|
||||
id: 'real_route',
|
||||
points: points,
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 6,
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
_fitMapToRoute();
|
||||
}
|
||||
|
||||
Future<void> _fitMapToRoute() async {
|
||||
final controller = _mapController;
|
||||
if (!mounted || controller == null) return;
|
||||
|
||||
await controller.fitBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(
|
||||
math.min(_pickupLocation.latitude, _dropLocation.latitude),
|
||||
math.min(_pickupLocation.longitude, _dropLocation.longitude),
|
||||
),
|
||||
northeast: LatLng(
|
||||
math.max(_pickupLocation.latitude, _dropLocation.latitude),
|
||||
math.max(_pickupLocation.longitude, _dropLocation.longitude),
|
||||
),
|
||||
),
|
||||
padding: 80,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
SizedBox.expand(
|
||||
child: DoormileMap(
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: _pickupLocation,
|
||||
zoom: 14.5,
|
||||
),
|
||||
markers: _markers,
|
||||
polylines: _polylines,
|
||||
onMapReady: (controller) {
|
||||
_mapController = controller;
|
||||
_fitMapToRoute();
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 16,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
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: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final double maxSheetHeight =
|
||||
MediaQuery.of(ctx).size.height * 0.35;
|
||||
// ignore: unused_local_variable
|
||||
final double allowedHeight = math.min(
|
||||
constraints.maxHeight,
|
||||
maxSheetHeight,
|
||||
);
|
||||
return ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Customer Details',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xxLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
// Just launch dialer; PiP is handled only from navigation screen
|
||||
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: 8),
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 8),
|
||||
_buildCustomerInfo(),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: _isNavigating
|
||||
? null
|
||||
: () async {
|
||||
// Prevent multiple clicks
|
||||
if (_isNavigating ||
|
||||
!mounted ||
|
||||
!context.mounted)
|
||||
return;
|
||||
|
||||
setState(() {
|
||||
_isNavigating = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Capture screen size before navigation
|
||||
final screenSize =
|
||||
MediaQuery.of(context).size *
|
||||
MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final dc = Get.put(
|
||||
DeliveriesController(),
|
||||
permanent: true,
|
||||
);
|
||||
final d = widget.delivery;
|
||||
final int deliveryId =
|
||||
int.tryParse(
|
||||
'${d['deliveryid'] ?? d['DeliveryId'] ?? 0}',
|
||||
) ??
|
||||
0;
|
||||
final String orderId =
|
||||
(d['orderid'] ?? d['OrderId'] ?? '')
|
||||
.toString();
|
||||
final int orderHeaderId =
|
||||
int.tryParse(
|
||||
'${d['orderheaderid'] ?? d['OrderHeaderId'] ?? 0}',
|
||||
) ??
|
||||
0;
|
||||
// Save ridertime start at the moment navigation is started
|
||||
try {
|
||||
if (deliveryId > 0) {
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
// 1) Save rider time start (existing behaviour)
|
||||
await prefs.setString(
|
||||
'ridertime_start_$deliveryId',
|
||||
DateTime.now().toIso8601String(),
|
||||
);
|
||||
// 2) Save ETA end time for this order so PiP timer can resume correctly
|
||||
final rawEta = d['eta'];
|
||||
int etaMinutes = 0;
|
||||
if (rawEta != null) {
|
||||
etaMinutes =
|
||||
int.tryParse(rawEta.toString()) ??
|
||||
0;
|
||||
}
|
||||
if (etaMinutes > 0) {
|
||||
final now = DateTime.now();
|
||||
final endTime =
|
||||
now
|
||||
.add(
|
||||
Duration(
|
||||
minutes: etaMinutes,
|
||||
),
|
||||
)
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000; // store seconds
|
||||
await prefs.setInt(
|
||||
'eta_endtime_$orderId',
|
||||
endTime,
|
||||
);
|
||||
debugPrint(
|
||||
'[ACTIVE][ETA] Saved eta_endtime_$orderId -> $endTime (eta=$etaMinutes min)',
|
||||
);
|
||||
}
|
||||
debugPrint(
|
||||
'[ACTIVE] Saved ridertime_start for deliveryId=$deliveryId',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error saving ridertime_start: $e',
|
||||
);
|
||||
}
|
||||
|
||||
final parentState =
|
||||
widget.parentState ??
|
||||
context
|
||||
.findAncestorStateOfType<
|
||||
_MyDeliveriesState
|
||||
>();
|
||||
|
||||
// ✅ CRITICAL: Check active delivery BEFORE navigation
|
||||
if (parentState != null) {
|
||||
final currentOrderId =
|
||||
(d['orderid'] ?? d['OrderId'] ?? '')
|
||||
.toString();
|
||||
final activeOrderIds = parentState
|
||||
._activeDeliveries
|
||||
.map(
|
||||
(del) =>
|
||||
(del['orderid'] ??
|
||||
del['OrderId'] ??
|
||||
'')
|
||||
.toString(),
|
||||
)
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// Block if there's a different active delivery
|
||||
if (parentState
|
||||
._activeDeliveries
|
||||
.isNotEmpty &&
|
||||
!activeOrderIds.contains(
|
||||
currentOrderId,
|
||||
)) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(
|
||||
24,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
20,
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end:
|
||||
Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.orange.shade400,
|
||||
Colors.red.shade500,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
decoration:
|
||||
BoxDecoration(
|
||||
color: Colors
|
||||
.white
|
||||
.withOpacity(
|
||||
0.2,
|
||||
),
|
||||
shape: BoxShape
|
||||
.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.warning_rounded,
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
// Title
|
||||
Text(
|
||||
'Active Delivery in Progress',
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize:
|
||||
FontConstants.xxLarge(
|
||||
context,
|
||||
),
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
fontFamily:
|
||||
FontConstants
|
||||
.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
// Message
|
||||
Text(
|
||||
'Please complete your active delivery first before starting another delivery.',
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white
|
||||
.withOpacity(
|
||||
0.95,
|
||||
),
|
||||
fontSize:
|
||||
FontConstants.medium(
|
||||
context,
|
||||
),
|
||||
height: 1.4,
|
||||
fontFamily:
|
||||
FontConstants
|
||||
.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// Action Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop();
|
||||
// Navigate to active delivery
|
||||
if (parentState
|
||||
._activeDeliveries
|
||||
.isNotEmpty) {
|
||||
parentState
|
||||
.startDelivery(
|
||||
parentState
|
||||
._activeDeliveries
|
||||
.first,
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Colors.white,
|
||||
foregroundColor:
|
||||
Colors
|
||||
.red
|
||||
.shade600,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons
|
||||
.two_wheeler,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
'View Active Delivery',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
FontConstants.regular(
|
||||
context,
|
||||
),
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold,
|
||||
fontFamily:
|
||||
FontConstants
|
||||
.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
// Close Button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Close',
|
||||
style: TextStyle(
|
||||
color: Colors.white
|
||||
.withOpacity(
|
||||
0.9,
|
||||
),
|
||||
fontSize: 15,
|
||||
fontWeight:
|
||||
FontWeight.w500,
|
||||
fontFamily:
|
||||
FontConstants
|
||||
.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return; // Return early to prevent starting another delivery
|
||||
}
|
||||
}
|
||||
|
||||
if (deliveryId > 0 && orderId.isNotEmpty) {
|
||||
String riderLatStr = '0';
|
||||
String riderLngStr = '0';
|
||||
try {
|
||||
final position =
|
||||
await Geolocator.getCurrentPosition(
|
||||
locationSettings:
|
||||
const LocationSettings(
|
||||
accuracy:
|
||||
LocationAccuracy.medium,
|
||||
timeLimit: Duration(
|
||||
seconds: 3,
|
||||
),
|
||||
),
|
||||
).timeout(
|
||||
const Duration(seconds: 3),
|
||||
);
|
||||
riderLatStr = position.latitude
|
||||
.toStringAsFixed(6);
|
||||
riderLngStr = position.longitude
|
||||
.toStringAsFixed(6);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error getting rider location: $e',
|
||||
);
|
||||
try {
|
||||
final lastPos =
|
||||
await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null) {
|
||||
riderLatStr = lastPos.latitude
|
||||
.toStringAsFixed(6);
|
||||
riderLngStr = lastPos.longitude
|
||||
.toStringAsFixed(6);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ✅ CRITICAL: Navigate FIRST, then handle status updates
|
||||
if (!mounted || !context.mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate immediately - this must happen
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
_RiderNavigationScreen(
|
||||
delivery: widget.delivery,
|
||||
parentState: widget.parentState,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Reset navigation state after navigation completes
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
|
||||
// Continue with status updates and PiP in background
|
||||
debugPrint(
|
||||
'[ACTIVE] Updating status for deliveryId=$deliveryId orderId=$orderId lat=$riderLatStr lng=$riderLngStr',
|
||||
);
|
||||
|
||||
final ok = await dc.updateActiveStatus(
|
||||
deliveryId: deliveryId,
|
||||
orderHeaderId: orderHeaderId,
|
||||
ridersLat: riderLatStr,
|
||||
ridersLng: riderLngStr,
|
||||
orderId: orderId,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE] Status update result: $ok',
|
||||
);
|
||||
|
||||
// ✅ CRITICAL: ENFORCE PiP when delivery becomes active (compulsory)
|
||||
if (ok && !dc.isPipEnabled.value) {
|
||||
try {
|
||||
debugPrint(
|
||||
'[ACTIVE] Delivery is now active - Enforcing PiP mode',
|
||||
);
|
||||
final floating = Floating();
|
||||
const rational = Rational.landscape();
|
||||
|
||||
final height =
|
||||
(screenSize.height * 0.5).toInt();
|
||||
final width = (screenSize.width * 0.9)
|
||||
.toInt();
|
||||
|
||||
final arguments = ImmediatePiP(
|
||||
aspectRatio: rational,
|
||||
sourceRectHint: math.Rectangle<int>(
|
||||
((screenSize.width - width) ~/ 2)
|
||||
.toInt(),
|
||||
((screenSize.height - height) ~/
|
||||
2)
|
||||
.toInt(),
|
||||
width,
|
||||
height,
|
||||
),
|
||||
);
|
||||
|
||||
await floating.enable(arguments);
|
||||
dc.isPipEnabled.value = true;
|
||||
debugPrint(
|
||||
'[ACTIVE] PiP enabled successfully',
|
||||
);
|
||||
|
||||
// Also try method channel as backup
|
||||
try {
|
||||
const channel = MethodChannel(
|
||||
'doormile/pip',
|
||||
);
|
||||
await channel.invokeMethod<bool>(
|
||||
'enterPip',
|
||||
);
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error enabling PiP: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentState != null) {
|
||||
final previousActive =
|
||||
parentState._activeDeliveryOrderId;
|
||||
if (previousActive != null &&
|
||||
previousActive != orderId) {
|
||||
parentState._stopDeliveryPosting(
|
||||
previousActive,
|
||||
);
|
||||
}
|
||||
|
||||
parentState._activeDeliveryOrderId =
|
||||
orderId;
|
||||
d['orderstatus'] = 'active';
|
||||
|
||||
final orderKey = parentState
|
||||
._getOrderKey(d);
|
||||
if (parentState._skippedOrdersCache
|
||||
.containsKey(orderKey)) {
|
||||
parentState._skippedOrdersCache
|
||||
.remove(orderKey);
|
||||
parentState._skippedOrderTimestamps
|
||||
.remove(orderKey);
|
||||
await parentState
|
||||
._saveSkippedOrdersCache();
|
||||
debugPrint(
|
||||
'[ACTIVE] Removed from skipped cache (resumed): $orderKey',
|
||||
);
|
||||
}
|
||||
|
||||
await parentState._startDeliveryPosting(
|
||||
d,
|
||||
);
|
||||
|
||||
try {
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
'active_delivery_order_id',
|
||||
orderId,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error saving active delivery ID: $e',
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: invalid_use_of_protected_member
|
||||
parentState.setState(() {});
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[ACTIVE] Invalid deliveryId: $deliveryId or orderId: $orderId',
|
||||
);
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error in navigation flow: $e',
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: _isNavigating
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Starting...',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
'Start Navigation',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomerInfo() {
|
||||
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()}',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildInfoRow(
|
||||
'Distance:',
|
||||
'${(widget.delivery['kms'] ?? '0').toString()} km',
|
||||
valueColor: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
String label,
|
||||
String value, {
|
||||
bool isExpanded = false,
|
||||
Color? valueColor,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
isExpanded
|
||||
? Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: valueColor ?? Colors.black87,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: valueColor ?? Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
40
lib/xpress/views/Dashboard/deliveries/map_btn.dart
Normal file
40
lib/xpress/views/Dashboard/deliveries/map_btn.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MAP VIEW BUTTON (Customer locations)
|
||||
// -------------------------------------------------------------------------
|
||||
class MapViewRow extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> deliveries;
|
||||
final Map<String, int>? preservedStepNumbers;
|
||||
|
||||
const MapViewRow({
|
||||
super.key,
|
||||
required this.deliveries,
|
||||
this.preservedStepNumbers,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MultiCustomerMapScreen(
|
||||
deliveries: deliveries,
|
||||
preservedStepNumbers: preservedStepNumbers ?? {},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/customermap.png',
|
||||
color: ColorConstants.primaryColor,
|
||||
height: 32,
|
||||
width: 32,
|
||||
errorBuilder: (c, e, s) =>
|
||||
const Icon(Icons.map, size: 32, color: Colors.blue),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
743
lib/xpress/views/Dashboard/deliveries/multi_map.dart
Normal file
743
lib/xpress/views/Dashboard/deliveries/multi_map.dart
Normal file
@@ -0,0 +1,743 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
class _StepPoint {
|
||||
final int step;
|
||||
final LatLng position;
|
||||
_StepPoint(this.step, this.position);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MULTI CUSTOMER MAP (Shows all deliveries with real step numbers)
|
||||
// -------------------------------------------------------------------------
|
||||
class MultiCustomerMapScreen extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> deliveries;
|
||||
final Map<String, int> preservedStepNumbers;
|
||||
|
||||
const MultiCustomerMapScreen({
|
||||
super.key,
|
||||
required this.deliveries,
|
||||
this.preservedStepNumbers = const {},
|
||||
});
|
||||
|
||||
@override
|
||||
State<MultiCustomerMapScreen> createState() => _MultiCustomerMapScreenState();
|
||||
}
|
||||
|
||||
class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
|
||||
DoormileMapController? mapController;
|
||||
List<DoormileMapMarker> markers = const [];
|
||||
List<DoormileMapPolyline> polylines = const [];
|
||||
LatLng? currentLocation;
|
||||
bool _isLoading = true;
|
||||
final Map<String, Map<String, dynamic>> _deliveryMap = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.deliveries.isNotEmpty) {
|
||||
final firstDelivery = widget.deliveries.first;
|
||||
final dropLat = _parseD(
|
||||
firstDelivery['droplat'] ?? firstDelivery['deliverylat'] ?? 0,
|
||||
);
|
||||
final dropLon = _parseD(
|
||||
firstDelivery['droplon'] ?? firstDelivery['deliverylong'] ?? 0,
|
||||
);
|
||||
if (dropLat != 0 && dropLon != 0) {
|
||||
currentLocation = LatLng(dropLat, dropLon);
|
||||
} else {
|
||||
currentLocation = const LatLng(11.018356, 77.012596);
|
||||
}
|
||||
} else {
|
||||
currentLocation = const LatLng(11.018356, 77.012596);
|
||||
}
|
||||
_loadMapData();
|
||||
}
|
||||
|
||||
double _parseD(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
int _getStepNumber(Map<String, dynamic> order) {
|
||||
final dynamic raw = order['step'] ?? order['Step'];
|
||||
final int step = raw == null
|
||||
? 0
|
||||
: (raw is num ? raw.toInt() : int.tryParse(raw.toString()) ?? 0);
|
||||
return step;
|
||||
}
|
||||
|
||||
String _getOrderKey(Map<String, dynamic> order) {
|
||||
final deliveryId = (order['deliveryid'] ?? '').toString();
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
return deliveryId.isNotEmpty ? 'delivery_$deliveryId' : 'order_$orderId';
|
||||
}
|
||||
|
||||
int _getPreservedOrCurrentStep(Map<String, dynamic> order) {
|
||||
final orderKey = _getOrderKey(order);
|
||||
if (widget.preservedStepNumbers.containsKey(orderKey)) {
|
||||
return widget.preservedStepNumbers[orderKey]!;
|
||||
}
|
||||
final currentStep = _getStepNumber(order);
|
||||
return currentStep;
|
||||
}
|
||||
|
||||
Future<void> _loadMapData() async {
|
||||
try {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
_getCurrentLocation().then((_) {
|
||||
if (mounted) {
|
||||
_createDeliveryMarkers();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Error loading map data: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getCurrentLocation() async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Position? position;
|
||||
try {
|
||||
position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.medium,
|
||||
distanceFilter: 0,
|
||||
timeLimit: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[CUSTOMER_MAP] Timeout getting location, using last known: $e',
|
||||
);
|
||||
position = await Geolocator.getLastKnownPosition();
|
||||
}
|
||||
|
||||
final safePosition = position;
|
||||
if (safePosition != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(
|
||||
safePosition.latitude,
|
||||
safePosition.longitude,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Error getting location: $e');
|
||||
try {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
int? _getNextDeliveryStep() {
|
||||
// Find the first non-skipped delivery (next delivery from user location)
|
||||
for (int i = 0; i < widget.deliveries.length; i++) {
|
||||
final delivery = widget.deliveries[i];
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
if (status != 'skipped') {
|
||||
final stepNumber = _getPreservedOrCurrentStep(delivery);
|
||||
if (stepNumber > 0) {
|
||||
return stepNumber;
|
||||
} else {
|
||||
// Calculate display step for orders without step
|
||||
final ordersWithStepBefore = widget.deliveries
|
||||
.sublist(0, i)
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
final totalOrdersWithStep = widget.deliveries
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
return totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _createDeliveryMarkers() async {
|
||||
final List<DoormileMapMarker> tempMarkers = [];
|
||||
List<_StepPoint> stepPoints = [];
|
||||
_deliveryMap.clear();
|
||||
|
||||
final nextStep = _getNextDeliveryStep();
|
||||
|
||||
for (int i = 0; i < widget.deliveries.length; i++) {
|
||||
final delivery = widget.deliveries[i];
|
||||
final stepNumber = _getPreservedOrCurrentStep(delivery);
|
||||
|
||||
int displayStep;
|
||||
if (stepNumber > 0) {
|
||||
displayStep = stepNumber;
|
||||
} else {
|
||||
final ordersWithStepBefore = widget.deliveries
|
||||
.sublist(0, i)
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
final totalOrdersWithStep = widget.deliveries
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
displayStep = totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
|
||||
}
|
||||
|
||||
final double lat = _parseD(
|
||||
delivery['droplat'] ?? delivery['deliverylat'],
|
||||
);
|
||||
final double lon = _parseD(
|
||||
delivery['droplon'] ?? delivery['deliverylong'],
|
||||
);
|
||||
if (lat == 0 || lon == 0) continue;
|
||||
|
||||
final orderId = (delivery['orderid'] ?? '').toString();
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isSkipped = status == 'skipped';
|
||||
final isNext = displayStep == nextStep;
|
||||
|
||||
// Store delivery data for dialog
|
||||
_deliveryMap['delivery_$orderId'] = delivery;
|
||||
|
||||
final icon = await MapMarkerIcons.numberedCircle(
|
||||
displayStep,
|
||||
primaryColor: ColorConstants.primaryColor,
|
||||
isSkipped: isSkipped,
|
||||
isNext: isNext,
|
||||
);
|
||||
|
||||
tempMarkers.add(
|
||||
DoormileMapMarker(
|
||||
id: 'delivery_$orderId',
|
||||
position: LatLng(lat, lon),
|
||||
icon: icon,
|
||||
anchor: MarkerAnchor.center,
|
||||
onTap: () {
|
||||
_showCustomerDetailsSheet(delivery, displayStep);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
stepPoints.add(_StepPoint(displayStep, LatLng(lat, lon)));
|
||||
}
|
||||
|
||||
if (currentLocation != null) {
|
||||
tempMarkers.add(
|
||||
DoormileMapMarker(
|
||||
id: 'current_location',
|
||||
position: currentLocation!,
|
||||
icon: await MapMarkerIcons.dot(Colors.blueAccent),
|
||||
anchor: MarkerAnchor.center,
|
||||
),
|
||||
);
|
||||
stepPoints.insert(0, _StepPoint(0, currentLocation!));
|
||||
}
|
||||
|
||||
stepPoints.sort((a, b) => a.step.compareTo(b.step));
|
||||
|
||||
final newPolylines = await _buildRoutePolylines(stepPoints);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
markers = tempMarkers;
|
||||
polylines = newPolylines;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
_fitBoundsToMarkersAndPolylines();
|
||||
}
|
||||
|
||||
/// Builds the route through every stop in one Directions call.
|
||||
///
|
||||
/// This used to issue one request per consecutive pair of stops, so opening
|
||||
/// this screen with N stops cost N billable requests. OSRM takes every stop
|
||||
/// as a coordinate in one request, so the whole route is a single call.
|
||||
Future<List<DoormileMapPolyline>> _buildRoutePolylines(
|
||||
List<_StepPoint> stepPoints,
|
||||
) async {
|
||||
if (stepPoints.length < 2) return const [];
|
||||
|
||||
final route = await RoutingService.getRoute(
|
||||
origin: stepPoints.first.position,
|
||||
destination: stepPoints.last.position,
|
||||
waypoints: stepPoints
|
||||
.sublist(1, stepPoints.length - 1)
|
||||
.map((s) => s.position)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
if (route != null && route.points.isNotEmpty) {
|
||||
return [
|
||||
DoormileMapPolyline(
|
||||
id: 'route',
|
||||
points: route.points,
|
||||
width: 6,
|
||||
color: Colors.blue,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
debugPrint('[CUSTOMER_MAP] Routing unavailable, drawing direct segments');
|
||||
return [
|
||||
DoormileMapPolyline(
|
||||
id: 'route_fallback',
|
||||
points: stepPoints.map((s) => s.position).toList(),
|
||||
width: 4,
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _fitBoundsToMarkersAndPolylines() async {
|
||||
final controller = mapController;
|
||||
if (controller == null) return;
|
||||
|
||||
double minLat = double.infinity;
|
||||
double maxLat = -double.infinity;
|
||||
double minLng = double.infinity;
|
||||
double maxLng = -double.infinity;
|
||||
|
||||
bool hasPoint = false;
|
||||
|
||||
void include(LatLng pos) {
|
||||
minLat = math.min(minLat, pos.latitude);
|
||||
maxLat = math.max(maxLat, pos.latitude);
|
||||
minLng = math.min(minLng, pos.longitude);
|
||||
maxLng = math.max(maxLng, pos.longitude);
|
||||
hasPoint = true;
|
||||
}
|
||||
|
||||
for (final m in markers) {
|
||||
include(m.position);
|
||||
}
|
||||
for (final poly in polylines) {
|
||||
for (final pos in poly.points) {
|
||||
include(pos);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPoint) return;
|
||||
|
||||
await controller.fitBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
),
|
||||
padding: 80,
|
||||
);
|
||||
}
|
||||
|
||||
void _showCustomerDetailsSheet(
|
||||
Map<String, dynamic> delivery,
|
||||
int stepNumber,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
final customerName = (delivery['deliverycustomer'] ?? 'Customer')
|
||||
.toString();
|
||||
final address = (delivery['deliveryaddress'] ?? 'Address not available')
|
||||
.toString();
|
||||
final phone = (delivery['deliverycontactno'] ?? '').toString();
|
||||
final orderId = (delivery['orderid'] ?? '').toString();
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isSkipped = status == 'skipped';
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with title and close icon
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Customer Details',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.cancel,
|
||||
size: 34,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Divider(thickness: 1.5),
|
||||
|
||||
// Step number and status
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSkipped
|
||||
? Colors.orange
|
||||
: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Step $stepNumber',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSkipped) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'SKIPPED',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Customer name
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.person, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Customer Name',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
customerName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Delivery address
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 20,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Delivery Address',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
address,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Phone number
|
||||
if (phone.isNotEmpty)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.phone, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Contact Number',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
phone,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Order ID
|
||||
Text(
|
||||
'Order ID: $orderId',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Call button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Close bottom sheet first
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Small delay to ensure bottom sheet is closed
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
// Then make the call
|
||||
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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
'Call',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
LatLng initialTarget;
|
||||
double initialZoom = 13;
|
||||
|
||||
if (currentLocation != null) {
|
||||
initialTarget = currentLocation!;
|
||||
} else if (widget.deliveries.isNotEmpty) {
|
||||
final firstDelivery = widget.deliveries.first;
|
||||
final lat = _parseD(
|
||||
firstDelivery['droplat'] ?? firstDelivery['deliverylat'],
|
||||
);
|
||||
final lon = _parseD(
|
||||
firstDelivery['droplon'] ?? firstDelivery['deliverylong'],
|
||||
);
|
||||
initialTarget = (lat != 0 && lon != 0)
|
||||
? LatLng(lat, lon)
|
||||
: const LatLng(11.0168, 76.9558);
|
||||
} else {
|
||||
initialTarget = const LatLng(11.0168, 76.9558);
|
||||
}
|
||||
|
||||
final initialCameraPosition = CameraPosition(
|
||||
target: initialTarget,
|
||||
zoom: initialZoom,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(70),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
elevation: 0,
|
||||
toolbarHeight: 80,
|
||||
leadingWidth: double.infinity,
|
||||
leading: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const Text(
|
||||
'Delivery Route',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: DoormileMap(
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
myLocationEnabled: true,
|
||||
onMapReady: (controller) {
|
||||
mapController = controller;
|
||||
_fitBoundsToMarkersAndPolylines();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1039
lib/xpress/views/Dashboard/deliveries/nav.dart
Normal file
1039
lib/xpress/views/Dashboard/deliveries/nav.dart
Normal file
File diff suppressed because it is too large
Load Diff
172
lib/xpress/views/Dashboard/deliveries/pip.dart
Normal file
172
lib/xpress/views/Dashboard/deliveries/pip.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PIP INFO CARD (Shown when PiP mode is enabled)
|
||||
// -------------------------------------------------------------------------
|
||||
class PipInfoCard extends StatefulWidget {
|
||||
final String orderId;
|
||||
final int etaMinutes; // ETA in minutes from API (always treat as minutes)
|
||||
|
||||
const PipInfoCard({
|
||||
super.key,
|
||||
required this.orderId,
|
||||
required this.etaMinutes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PipInfoCard> createState() => _PipInfoCardState();
|
||||
}
|
||||
|
||||
class _PipInfoCardState extends State<PipInfoCard> {
|
||||
late final CountDownController _controller;
|
||||
int _durationSeconds = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CountDownController();
|
||||
// Load remaining ETA from SharedPreferences so timer doesn't reset
|
||||
_initDuration();
|
||||
}
|
||||
|
||||
Future<void> _initDuration() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final endKey = 'eta_endtime_${widget.orderId}';
|
||||
final endSeconds = prefs.getInt(endKey);
|
||||
final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
int remaining = 0;
|
||||
if (endSeconds != null && endSeconds > nowSeconds) {
|
||||
remaining = endSeconds - nowSeconds;
|
||||
} else {
|
||||
// Fallback: use full ETA from API
|
||||
final int safeEtaMinutes = widget.etaMinutes < 0
|
||||
? 0
|
||||
: widget.etaMinutes;
|
||||
remaining = safeEtaMinutes * 60;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_durationSeconds = remaining.clamp(0, 24 * 60 * 60);
|
||||
});
|
||||
} catch (_) {
|
||||
// In case of error, just fall back to raw ETA minutes
|
||||
final int safeEtaMinutes = widget.etaMinutes < 0 ? 0 : widget.etaMinutes;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_durationSeconds = safeEtaMinutes * 60;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// Expanded PiP: timer + key delivery details, but still relatively compact
|
||||
body: Center(
|
||||
child: widget.orderId.isEmpty || widget.orderId == 'N/A'
|
||||
? Text(
|
||||
'No Active Order',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
)
|
||||
: Card(
|
||||
color: Colors.white,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 6,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Timer
|
||||
SizedBox(
|
||||
width: 72,
|
||||
height: 72,
|
||||
child: _durationSeconds <= 0
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 3,
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Out',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CircularCountDownTimer(
|
||||
duration: _durationSeconds,
|
||||
initialDuration: 0,
|
||||
controller: _controller,
|
||||
width: 72,
|
||||
height: 72,
|
||||
ringColor: ColorConstants.primaryColor
|
||||
.withValues(alpha: 0.15),
|
||||
fillColor: ColorConstants.primaryColor,
|
||||
backgroundColor: Colors.white,
|
||||
strokeWidth: 5,
|
||||
strokeCap: StrokeCap.round,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 17,
|
||||
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(height: 4),
|
||||
// Active order id
|
||||
Text(
|
||||
'Active Order ID: ${widget.orderId}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1034
lib/xpress/views/Dashboard/deliveries/sheet.dart
Normal file
1034
lib/xpress/views/Dashboard/deliveries/sheet.dart
Normal file
File diff suppressed because it is too large
Load Diff
430
lib/xpress/views/Dashboard/deliveries/skip_sheet.dart
Normal file
430
lib/xpress/views/Dashboard/deliveries/skip_sheet.dart
Normal file
@@ -0,0 +1,430 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SKIP REASONS SHEET
|
||||
// -------------------------------------------------------------------------
|
||||
Future<void> _showMyOptionsSheet(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> delivery,
|
||||
_MyDeliveriesState? parentState,
|
||||
) async {
|
||||
int selected = -1;
|
||||
bool isLoading = false;
|
||||
|
||||
final List<String> skipReasons = [
|
||||
'Customer unreachable',
|
||||
'Customer not at the location',
|
||||
];
|
||||
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
Widget optionBox(String title, int index) {
|
||||
final bool isSelected = selected == index;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!isLoading) {
|
||||
setState(() => selected = index);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor.withOpacity(0.1)
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleConfirm() async {
|
||||
if (selected == -1 || isLoading) return;
|
||||
|
||||
// Check skip limits before proceeding
|
||||
final dc = Get.put(DeliveriesController(), permanent: true);
|
||||
final skipStatus = await dc.checkSkipStatus();
|
||||
final int skipCount = skipStatus['count'] ?? 0;
|
||||
bool applyPenalty = false;
|
||||
|
||||
if (skipCount >= 2) {
|
||||
// Show Styled Alert Dialog
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
elevation: 5,
|
||||
backgroundColor: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Colors.red.shade600,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Skip Limit Reached',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'You have exceeded the limit of 2 skips within 3 hours.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.orange.shade200,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: Colors.orange.shade800,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Proceeding will forfeit your bonus points for this session.",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.orange.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(context, false),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade600,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Confirm Skip",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm != true) return; // User cancelled or dismissed
|
||||
applyPenalty = true;
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
|
||||
try {
|
||||
final dc = Get.put(DeliveriesController(), permanent: true);
|
||||
final d = delivery;
|
||||
final int deliveryId =
|
||||
int.tryParse('${d['deliveryid'] ?? 0}') ?? 0;
|
||||
final int orderHeaderId =
|
||||
int.tryParse('${d['orderheaderid'] ?? 0}') ?? 0;
|
||||
final String reason =
|
||||
selected >= 0 && selected < skipReasons.length
|
||||
? skipReasons[selected]
|
||||
: '';
|
||||
|
||||
if (deliveryId > 0 && orderHeaderId > 0) {
|
||||
String riderLatStr = '0';
|
||||
String riderLngStr = '0';
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
timeLimit: const Duration(seconds: 5),
|
||||
);
|
||||
riderLatStr = position.latitude.toStringAsFixed(6);
|
||||
riderLngStr = position.longitude.toStringAsFixed(6);
|
||||
} catch (e) {
|
||||
debugPrint('[SKIP] Error getting rider location: $e');
|
||||
try {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null) {
|
||||
riderLatStr = lastPos.latitude.toStringAsFixed(6);
|
||||
riderLngStr = lastPos.longitude.toStringAsFixed(6);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[SKIP] Updating status for deliveryId=$deliveryId orderHeaderId=$orderHeaderId reason="$reason"',
|
||||
);
|
||||
|
||||
final ok = await dc.updateSkippedStatus(
|
||||
deliveryId: deliveryId,
|
||||
orderHeaderId: orderHeaderId,
|
||||
ridersLat: riderLatStr,
|
||||
ridersLng: riderLngStr,
|
||||
notes: reason,
|
||||
);
|
||||
|
||||
debugPrint('[SKIP] Status update result: $ok');
|
||||
|
||||
if (ok && context.mounted) {
|
||||
// Register skip locally only on success
|
||||
await dc.registerSkip(applyPenalty: applyPenalty);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // Close the sheet
|
||||
|
||||
if (parentState != null) {
|
||||
// Only update list if called from the list view
|
||||
parentState.markOrderAsSkipped(d, reason);
|
||||
} else {
|
||||
// If called from Navigation/Map (where parentState is null),
|
||||
// we just close the sheet and let the caller handle the UI update
|
||||
// typically by popping the route or showing a snackbar.
|
||||
// We DO NOT force navigation to "MyDeliveries".
|
||||
debugPrint(
|
||||
'[SKIP] Skipped from Nav/Map screen, sheet closed.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Failed to skip delivery. Please try again.',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[SKIP] Invalid deliveryId: $deliveryId or orderHeaderId: $orderHeaderId',
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Invalid delivery information.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[SKIP] Error updating skip status: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('An error occurred. Please try again.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 30),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Select Reason',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.cancel,
|
||||
color: Colors.red,
|
||||
size: 32,
|
||||
),
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
optionBox('Customer unreachable', 0),
|
||||
optionBox('Customer not at the location', 1),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: (selected == -1 || isLoading)
|
||||
? Colors.grey.shade300
|
||||
: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: (selected == -1 || isLoading)
|
||||
? null
|
||||
: handleConfirm,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Confirm',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: (selected == -1 || isLoading)
|
||||
? Colors.black45
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user