initial commit: push everything

This commit is contained in:
2026-07-06 20:27:53 +05:30
commit df4e044d74
329 changed files with 36620 additions and 0 deletions

View 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,
),
),
),
),
),
),
),
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,278 @@
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
image: Image.asset(
'assets/images/nearlelauncher.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;
}
}

View File

@@ -0,0 +1,831 @@
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> {
GoogleMapController? _mapController;
late final LatLng _pickupLocation;
late final LatLng _dropLocation;
final Set<Marker> _markers = {};
final Set<Polyline> _polylines = {};
late final PolylinePoints _polylinePoints;
bool _isLoadingRoute = true;
bool _isNavigating = false; // Prevent multiple clicks
static const String _googleApiKey = 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q';
@override
void initState() {
super.initState();
_polylinePoints = PolylinePoints(apiKey: _googleApiKey);
_resolveLocationsFromDelivery();
_setMarkers();
_isLoadingRoute = false;
_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;
}
void _setMarkers() {
_markers.addAll([
Marker(
markerId: const MarkerId('pickup'),
position: _pickupLocation,
infoWindow: const InfoWindow(title: 'Pickup Location'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
),
Marker(
markerId: const MarkerId('drop'),
position: _dropLocation,
infoWindow: const InfoWindow(title: 'Drop Location'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
),
]);
}
Future<void> _createRealRoute() async {
try {
final request = PolylineRequest(
origin: PointLatLng(
_pickupLocation.latitude,
_pickupLocation.longitude,
),
destination: PointLatLng(
_dropLocation.latitude,
_dropLocation.longitude,
),
mode: TravelMode.driving,
);
final result = await _polylinePoints.getRouteBetweenCoordinates(
request: request,
);
if (result.points.isNotEmpty) {
final routePoints = result.points
.map((e) => LatLng(e.latitude, e.longitude))
.toList();
if (mounted) {
setState(() {
_polylines.add(
Polyline(
polylineId: const PolylineId('real_route'),
color: ColorConstants.primaryColor,
width: 6,
points: routePoints,
),
);
_isLoadingRoute = false;
});
_fitMapToRoute();
}
} else {
debugPrint('[MAP_PREVIEW] No route found');
if (mounted) {
setState(() => _isLoadingRoute = false);
}
}
} catch (e) {
debugPrint('[MAP_PREVIEW] Error creating route: $e');
if (mounted) {
setState(() => _isLoadingRoute = false);
}
}
}
Future<void> _fitMapToRoute() async {
if (!mounted) return;
if (_mapController == null) return;
// Check if controller is still alive (important!)
try {
await _mapController!.getVisibleRegion();
} catch (e) {
debugPrint("❌ Map controller is dead. Skip animateCamera.");
return;
}
final bounds = 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),
),
);
// Try animate safely
for (int i = 0; i < 10; i++) {
if (!mounted) return;
try {
await _mapController!.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 80),
);
return;
} catch (e) {
await Future.delayed(const Duration(milliseconds: 150));
}
}
debugPrint("❌ animateCamera failed after retries (map probably disposed)");
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
SizedBox.expand(
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: _pickupLocation,
zoom: 14.5,
),
onMapCreated: (controller) {
_mapController = controller;
Future.delayed(const Duration(milliseconds: 500), () {
if (!_isLoadingRoute && _polylines.isNotEmpty) {
_fitMapToRoute();
}
});
},
markers: _markers,
polylines: _polylines,
zoomControlsEnabled: false,
myLocationButtonEnabled: false,
),
),
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('nearle/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,
),
),
],
);
}
}

View File

@@ -0,0 +1,41 @@
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),
),
);
}
}

View File

@@ -0,0 +1,894 @@
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> {
GoogleMapController? mapController;
Set<Marker> markers = {};
Set<Polyline> polylines = {};
LatLng? currentLocation;
bool _isLoading = true;
bool _mapReady = false;
final PolylinePoints _polylinePoints = PolylinePoints(
apiKey: 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q',
);
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 {
Set<Marker> 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 customerName = (delivery['deliverycustomer'] ?? 'Customer ${i + 1}')
.toString();
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 _createCircularMarkerBitmap(
displayStep,
isSkipped: isSkipped,
isNext: isNext,
);
tempMarkers.add(
Marker(
markerId: MarkerId('delivery_$orderId'),
position: LatLng(lat, lon),
icon: icon,
infoWindow: InfoWindow(
title: isSkipped
? 'Step $displayStep: $customerName (SKIPPED)'
: 'Step $displayStep: $customerName',
snippet: 'Order #$orderId',
),
onTap: () {
_showCustomerDetailsSheet(delivery, displayStep);
},
),
);
stepPoints.add(_StepPoint(displayStep, LatLng(lat, lon)));
}
if (currentLocation != null) {
tempMarkers.add(
Marker(
markerId: const MarkerId('current_location'),
position: currentLocation!,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueAzure,
),
infoWindow: const InfoWindow(title: 'You are here'),
),
);
stepPoints.insert(0, _StepPoint(0, currentLocation!));
}
stepPoints.sort((a, b) => a.step.compareTo(b.step));
Set<Polyline> newPolylines = {};
int segmentIndex = 0;
for (int i = 0; i < stepPoints.length - 1; i++) {
final start = stepPoints[i].position;
final end = stepPoints[i + 1].position;
try {
// ignore: deprecated_member_use
final request = PolylineRequest(
origin: PointLatLng(start.latitude, start.longitude),
destination: PointLatLng(end.latitude, end.longitude),
mode: TravelMode.driving,
);
final result = await _polylinePoints.getRouteBetweenCoordinates(
request: request,
);
if (result.points.isNotEmpty) {
final routePoints = result.points
.map((p) => LatLng(p.latitude, p.longitude))
.toList();
newPolylines.add(
Polyline(
polylineId: PolylineId('segment_$segmentIndex'),
points: routePoints,
width: 6,
color: Colors.blue,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
jointType: JointType.round,
geodesic: true,
),
);
segmentIndex++;
} else {
newPolylines.add(
Polyline(
polylineId: PolylineId('segment_fallback_$segmentIndex'),
points: [start, end],
width: 4,
color: Colors.blue.shade200,
),
);
segmentIndex++;
}
} catch (e) {
debugPrint('[CUSTOMER_MAP] Directions error for segment $i: $e');
newPolylines.add(
Polyline(
polylineId: PolylineId('segment_error_$segmentIndex'),
points: [start, end],
width: 4,
color: Colors.blue.shade200,
),
);
segmentIndex++;
}
}
if (mounted) {
setState(() {
markers = tempMarkers;
polylines = newPolylines;
});
}
await Future.delayed(const Duration(milliseconds: 200));
_fitBoundsToMarkersAndPolylines();
}
Future<void> _fitBoundsToMarkersAndPolylines() async {
if (mapController == null) return;
double minLat = double.infinity;
double maxLat = -double.infinity;
double minLng = double.infinity;
double maxLng = -double.infinity;
bool hasPoint = false;
for (final m in markers) {
final pos = m.position;
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 poly in polylines) {
for (final pos in poly.points) {
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;
}
}
if (!hasPoint) return;
final bounds = LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
await mapController!.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 80),
);
} catch (e) {
Future.delayed(const Duration(milliseconds: 300), () async {
try {
await mapController!.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 80),
);
} catch (e) {
debugPrint('[CUSTOMER_MAP] Retry failed: $e');
}
});
}
});
}
Future<BitmapDescriptor> _createCircularMarkerBitmap(
int number, {
bool isSkipped = false,
bool isNext = false,
}) async {
const double size = 70;
final pictureRecorder = ui.PictureRecorder();
final canvas = Canvas(pictureRecorder);
final center = Offset(size / 2, size / 2);
final paint = Paint()
..color = isSkipped ? Colors.orange : ColorConstants.primaryColor;
canvas.drawCircle(center, 15, paint);
final border = Paint()
..color = isSkipped ? Colors.orange.shade900 : Colors.white
..style = PaintingStyle.stroke
..strokeWidth = isSkipped ? 4 : 3;
canvas.drawCircle(center, 15, border);
final textPainter = TextPainter(
text: TextSpan(
text: number.toString(),
style: const TextStyle(
fontSize: 19,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
center.dx - textPainter.width / 2,
center.dy - textPainter.height / 2,
),
);
// Draw "NEXT" indicator (road sign with down arrow) on top
if (isNext) {
// Draw road sign background (rectangle)
final signPaint = Paint()
..color = Colors.green
..style = PaintingStyle.fill;
final signRect = RRect.fromRectAndRadius(
Rect.fromCenter(center: Offset(size / 2, 8), width: 45, height: 30),
const Radius.circular(4),
);
canvas.drawRRect(signRect, signPaint);
// Draw border
final signBorder = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
canvas.drawRRect(signRect, signBorder);
// Draw "NEXT" text
final nextTextPainter = TextPainter(
text: const TextSpan(
text: 'NEXT',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
nextTextPainter.layout();
nextTextPainter.paint(
canvas,
Offset(size / 2 - nextTextPainter.width / 2, 4),
);
// Draw down arrow
final arrowPath = Path();
arrowPath.moveTo(size / 2, 18);
arrowPath.lineTo(size / 2 - 4, 24);
arrowPath.lineTo(size / 2 + 4, 24);
arrowPath.close();
final arrowPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawPath(arrowPath, arrowPaint);
}
final img = await pictureRecorder.endRecording().toImage(
size.toInt(),
size.toInt(),
);
final data = await img.toByteData(format: ui.ImageByteFormat.png);
return BitmapDescriptor.bytes(data!.buffer.asUint8List());
}
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())
: GoogleMap(
initialCameraPosition: initialCameraPosition,
markers: markers,
polylines: polylines,
myLocationEnabled: true,
myLocationButtonEnabled: true,
onMapCreated: (controller) {
mapController = controller;
_mapReady = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 150), () {
if (_mapReady) _fitBoundsToMarkersAndPolylines();
});
});
},
),
);
}
}

File diff suppressed because it is too large Load Diff

View 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,
),
),
],
),
),
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,411 @@
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,
),
),
),
),
],
),
);
},
),
);
},
);
}