You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
639 lines
17 KiB
639 lines
17 KiB
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:supplier_new/common/settings.dart';
|
|
import 'package:supplier_new/resources/source_loctaions_model.dart';
|
|
import '../resources/drivers_model.dart';
|
|
import '../resources/tankers_model.dart';
|
|
|
|
class AssignDriverScreenNewDesign extends StatefulWidget {
|
|
final dynamic order;
|
|
final dynamic status;
|
|
|
|
const AssignDriverScreenNewDesign({
|
|
super.key,
|
|
this.order,
|
|
this.status,
|
|
});
|
|
|
|
@override
|
|
State<AssignDriverScreenNewDesign> createState() =>
|
|
_AssignDriverScreenNewDesignState();
|
|
}
|
|
|
|
class _AssignDriverScreenNewDesignState
|
|
extends State<AssignDriverScreenNewDesign> {
|
|
GoogleMapController? _mapController;
|
|
|
|
List<DriversModel> driversList = [];
|
|
List<TankersModel> tankersList = [];
|
|
List<SourceLocationsModel> sourceLocationsList = [];
|
|
|
|
DriversModel? selectedDriver;
|
|
TankersModel? selectedTanker;
|
|
SourceLocationsModel? selectedSource;
|
|
|
|
bool loading = true;
|
|
bool routeLoading = false;
|
|
|
|
Set<Marker> _markers = {};
|
|
Set<Polyline> _polylines = {};
|
|
|
|
String eta = '';
|
|
String distanceText = '';
|
|
String routeError = '';
|
|
|
|
static const String googleApiKey = 'AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
loadData();
|
|
}
|
|
|
|
Future<void> loadData() async {
|
|
try {
|
|
final d = await AppSettings.getDrivers();
|
|
driversList = (jsonDecode(d)['data'] as List)
|
|
.map((e) => DriversModel.fromJson(e))
|
|
.toList();
|
|
|
|
final t = await AppSettings.getTankers();
|
|
tankersList = (jsonDecode(t)['data'] as List)
|
|
.map((e) => TankersModel.fromJson(e))
|
|
.toList();
|
|
|
|
final s = await AppSettings.getSourceLoctaions();
|
|
sourceLocationsList = (jsonDecode(s)['data'] as List)
|
|
.map((e) => SourceLocationsModel.fromJson(e))
|
|
.toList();
|
|
} catch (e) {
|
|
debugPrint('Load error: $e');
|
|
}
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
loading = false;
|
|
_rebuildMarkers();
|
|
});
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_fitMap();
|
|
});
|
|
}
|
|
|
|
double? _toDouble(dynamic value) {
|
|
if (value == null) return null;
|
|
if (value is double) return value;
|
|
if (value is int) return value.toDouble();
|
|
return double.tryParse(value.toString().trim());
|
|
}
|
|
|
|
LatLng? _deliveryPosition() {
|
|
try {
|
|
final lat = _toDouble(
|
|
widget.order?.lat ??
|
|
widget.order?.delivery_lat ??
|
|
widget.order?.location_lat,
|
|
);
|
|
final lng = _toDouble(
|
|
widget.order?.lng ??
|
|
widget.order?.delivery_lng ??
|
|
widget.order?.location_lng,
|
|
);
|
|
|
|
if (lat == null || lng == null) return null;
|
|
return LatLng(lat, lng);
|
|
} catch (e) {
|
|
debugPrint('Delivery parse error: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
LatLng? _sourcePosition(SourceLocationsModel source) {
|
|
try {
|
|
final lat = _toDouble(source.latitude);
|
|
final lng = _toDouble(source.longitude);
|
|
if (lat == null || lng == null) return null;
|
|
return LatLng(lat, lng);
|
|
} catch (e) {
|
|
debugPrint('Source parse error: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void _rebuildMarkers() {
|
|
final Set<Marker> markers = {};
|
|
|
|
final delivery = _deliveryPosition();
|
|
if (delivery != null) {
|
|
markers.add(
|
|
Marker(
|
|
markerId: const MarkerId('delivery'),
|
|
position: delivery,
|
|
icon: BitmapDescriptor.defaultMarkerWithHue(
|
|
BitmapDescriptor.hueRed,
|
|
),
|
|
infoWindow: InfoWindow(
|
|
title: widget.order?.building_name?.toString() ?? 'Delivery',
|
|
snippet: 'Customer location',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
for (final source in sourceLocationsList) {
|
|
final pos = _sourcePosition(source);
|
|
if (pos == null) continue;
|
|
|
|
final bool isSelected = selectedSource?.dbId == source.dbId;
|
|
|
|
markers.add(
|
|
Marker(
|
|
markerId: MarkerId('source_${source.dbId}'),
|
|
position: pos,
|
|
icon: BitmapDescriptor.defaultMarkerWithHue(
|
|
isSelected
|
|
? BitmapDescriptor.hueViolet
|
|
: BitmapDescriptor.hueAzure,
|
|
),
|
|
infoWindow: InfoWindow(
|
|
title: source.source_name?.toString() ?? 'Source',
|
|
snippet: isSelected ? 'Selected source' : 'Tap to select source',
|
|
),
|
|
onTap: () async {
|
|
setState(() {
|
|
selectedSource = source;
|
|
routeError = '';
|
|
eta = '';
|
|
distanceText = '';
|
|
_rebuildMarkers();
|
|
});
|
|
|
|
await _loadRouteAndEta();
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_fitMap();
|
|
});
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
_markers = markers;
|
|
}
|
|
|
|
Future<void> _loadRouteAndEta() async {
|
|
final source = selectedSource;
|
|
final sourcePos = source == null ? null : _sourcePosition(source);
|
|
final deliveryPos = _deliveryPosition();
|
|
|
|
if (sourcePos == null || deliveryPos == null) {
|
|
setState(() {
|
|
routeError = 'Source or delivery coordinates missing';
|
|
_polylines = {};
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
routeLoading = true;
|
|
routeError = '';
|
|
});
|
|
|
|
try {
|
|
await Future.wait([
|
|
_loadDirectionsPolyline(sourcePos, deliveryPos),
|
|
_loadEtaWithDirections(sourcePos, deliveryPos),
|
|
]);
|
|
} catch (e) {
|
|
debugPrint('Route load error: $e');
|
|
if (mounted) {
|
|
setState(() {
|
|
routeError = 'Unable to load route';
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!mounted) return;
|
|
setState(() {
|
|
routeLoading = false;
|
|
});
|
|
}
|
|
|
|
Future<void> _loadEtaWithDirections(
|
|
LatLng origin,
|
|
LatLng destination,
|
|
) async {
|
|
try {
|
|
final uri = Uri.parse(
|
|
'https://maps.googleapis.com/maps/api/directions/json'
|
|
'?origin=${origin.latitude},${origin.longitude}'
|
|
'&destination=${destination.latitude},${destination.longitude}'
|
|
'&mode=driving'
|
|
'&departure_time=now'
|
|
'&traffic_model=best_guess'
|
|
'&key=$googleApiKey',
|
|
);
|
|
|
|
final response = await http.get(uri);
|
|
final data = jsonDecode(response.body);
|
|
|
|
debugPrint('Directions ETA response: $data');
|
|
|
|
if (data['status'] != 'OK') {
|
|
setState(() {
|
|
routeError = 'ETA unavailable: ${data['status']}';
|
|
});
|
|
return;
|
|
}
|
|
|
|
final routes = data['routes'] as List?;
|
|
if (routes == null || routes.isEmpty) {
|
|
setState(() {
|
|
routeError = 'ETA unavailable';
|
|
});
|
|
return;
|
|
}
|
|
|
|
final legs = routes.first['legs'] as List?;
|
|
if (legs == null || legs.isEmpty) {
|
|
setState(() {
|
|
routeError = 'ETA unavailable';
|
|
});
|
|
return;
|
|
}
|
|
|
|
final leg = legs.first;
|
|
final distance = leg['distance'];
|
|
final duration = leg['duration'];
|
|
final durationInTraffic = leg['duration_in_traffic'];
|
|
|
|
setState(() {
|
|
distanceText = (distance?['text'] ?? '').toString();
|
|
eta = (durationInTraffic?['text'] ?? duration?['text'] ?? '').toString();
|
|
});
|
|
} catch (e) {
|
|
debugPrint('ETA error: $e');
|
|
setState(() {
|
|
routeError = 'ETA request failed';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _loadDirectionsPolyline(
|
|
LatLng origin,
|
|
LatLng destination,
|
|
) async {
|
|
try {
|
|
final uri = Uri.parse(
|
|
'https://maps.googleapis.com/maps/api/directions/json'
|
|
'?origin=${origin.latitude},${origin.longitude}'
|
|
'&destination=${destination.latitude},${destination.longitude}'
|
|
'&mode=driving'
|
|
'&departure_time=now'
|
|
'&traffic_model=best_guess'
|
|
'&key=$googleApiKey',
|
|
);
|
|
|
|
final response = await http.get(uri);
|
|
final data = jsonDecode(response.body);
|
|
|
|
debugPrint('Directions route response: $data');
|
|
|
|
if (data['status'] != 'OK') {
|
|
setState(() {
|
|
routeError = 'Route unavailable: ${data['status']}';
|
|
_polylines = {};
|
|
});
|
|
return;
|
|
}
|
|
|
|
final routes = data['routes'] as List?;
|
|
if (routes == null || routes.isEmpty) {
|
|
setState(() {
|
|
routeError = 'No route found';
|
|
_polylines = {};
|
|
});
|
|
return;
|
|
}
|
|
|
|
final encoded = routes.first['overview_polyline']?['points']?.toString();
|
|
if (encoded == null || encoded.isEmpty) {
|
|
setState(() {
|
|
routeError = 'Route polyline missing';
|
|
_polylines = {};
|
|
});
|
|
return;
|
|
}
|
|
|
|
final polylinePoints = PolylinePoints();
|
|
final decoded = polylinePoints.decodePolyline(encoded);
|
|
|
|
final points = decoded
|
|
.map((p) => LatLng(p.latitude, p.longitude))
|
|
.toList();
|
|
|
|
if (points.isEmpty) {
|
|
setState(() {
|
|
routeError = 'Failed to decode route';
|
|
_polylines = {};
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_polylines = {
|
|
Polyline(
|
|
polylineId: const PolylineId('source_delivery_route'),
|
|
points: points,
|
|
color: Colors.blue,
|
|
width: 6,
|
|
),
|
|
};
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Polyline error: $e');
|
|
setState(() {
|
|
routeError = 'Route request failed';
|
|
_polylines = {};
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _fitMap() async {
|
|
if (_mapController == null) return;
|
|
|
|
final List<LatLng> points = [];
|
|
|
|
final delivery = _deliveryPosition();
|
|
if (delivery != null) points.add(delivery);
|
|
|
|
for (final source in sourceLocationsList) {
|
|
final pos = _sourcePosition(source);
|
|
if (pos != null) points.add(pos);
|
|
}
|
|
|
|
if (points.isEmpty) return;
|
|
|
|
if (points.length == 1) {
|
|
await _mapController!.animateCamera(
|
|
CameraUpdate.newCameraPosition(
|
|
CameraPosition(target: points.first, zoom: 14),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
double minLat = points.first.latitude;
|
|
double maxLat = points.first.latitude;
|
|
double minLng = points.first.longitude;
|
|
double maxLng = points.first.longitude;
|
|
|
|
for (final point in points) {
|
|
if (point.latitude < minLat) minLat = point.latitude;
|
|
if (point.latitude > maxLat) maxLat = point.latitude;
|
|
if (point.longitude < minLng) minLng = point.longitude;
|
|
if (point.longitude > maxLng) maxLng = point.longitude;
|
|
}
|
|
|
|
try {
|
|
await _mapController!.animateCamera(
|
|
CameraUpdate.newLatLngBounds(
|
|
LatLngBounds(
|
|
southwest: LatLng(minLat, minLng),
|
|
northeast: LatLng(maxLat, maxLng),
|
|
),
|
|
80,
|
|
),
|
|
);
|
|
} catch (_) {
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
try {
|
|
await _mapController!.animateCamera(
|
|
CameraUpdate.newLatLngBounds(
|
|
LatLngBounds(
|
|
southwest: LatLng(minLat, minLng),
|
|
northeast: LatLng(maxLat, maxLng),
|
|
),
|
|
80,
|
|
),
|
|
);
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
Future<void> assign() async {
|
|
if (selectedDriver == null) {
|
|
_msg('Select driver');
|
|
return;
|
|
}
|
|
|
|
if (selectedTanker == null) {
|
|
_msg('Select tanker');
|
|
return;
|
|
}
|
|
|
|
if (selectedSource == null) {
|
|
_msg('Select source from map');
|
|
return;
|
|
}
|
|
|
|
final payload = <String, dynamic>{};
|
|
payload["tankerName"] = selectedTanker!.tanker_name;
|
|
payload["delivery_agent"] = selectedDriver!.driver_name;
|
|
payload["delivery_agent_mobile"] = selectedDriver!.phone_number;
|
|
payload["water_source_location"] = selectedSource!.source_name;
|
|
|
|
AppSettings.preLoaderDialog(context);
|
|
|
|
bool status = false;
|
|
try {
|
|
status = await AppSettings.assignTanker(payload, widget.order.dbId);
|
|
} catch (e) {
|
|
debugPrint('Assign error: $e');
|
|
}
|
|
|
|
if (mounted) {
|
|
Navigator.pop(context);
|
|
}
|
|
|
|
if (!mounted) return;
|
|
|
|
if (status) {
|
|
_msg('Assigned successfully');
|
|
Navigator.pop(context, true);
|
|
} else {
|
|
_msg('Assignment failed');
|
|
}
|
|
}
|
|
|
|
void _msg(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message)),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoRow(IconData icon, String text) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 18),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: Text(text)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomCard() {
|
|
return Container(
|
|
margin: const EdgeInsets.all(15),
|
|
padding: const EdgeInsets.all(15),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(18),
|
|
boxShadow: const [
|
|
BoxShadow(
|
|
blurRadius: 10,
|
|
color: Colors.black26,
|
|
offset: Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
DropdownButton<DriversModel>(
|
|
isExpanded: true,
|
|
hint: const Text('Select Driver'),
|
|
value: selectedDriver,
|
|
items: driversList.map((driver) {
|
|
return DropdownMenuItem(
|
|
value: driver,
|
|
child: Text(driver.driver_name?.toString() ?? 'Driver'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
selectedDriver = value;
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
DropdownButton<TankersModel>(
|
|
isExpanded: true,
|
|
hint: const Text('Select Tanker'),
|
|
value: selectedTanker,
|
|
items: tankersList.map((tanker) {
|
|
return DropdownMenuItem(
|
|
value: tanker,
|
|
child: Text(
|
|
'${tanker.tanker_name} (${tanker.capacity})',
|
|
),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
selectedTanker = value;
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildInfoRow(
|
|
Icons.water_drop_outlined,
|
|
selectedSource?.source_name?.toString() ?? 'Select source from map',
|
|
),
|
|
if (routeLoading) ...[
|
|
const SizedBox(height: 8),
|
|
const LinearProgressIndicator(),
|
|
],
|
|
if (distanceText.isNotEmpty || eta.isNotEmpty) ...[
|
|
const SizedBox(height: 10),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0XFFF5F4FF),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
if (distanceText.isNotEmpty)
|
|
_buildInfoRow(Icons.route, 'Distance: $distanceText'),
|
|
if (eta.isNotEmpty)
|
|
_buildInfoRow(Icons.timer_outlined, 'ETA: $eta'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
if (routeError.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
routeError,
|
|
style: const TextStyle(
|
|
color: Colors.red,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: assign,
|
|
child: const Text('ASSIGN'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (loading) {
|
|
return const Scaffold(
|
|
body: Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Enterprise Dispatch Map'),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
GoogleMap(
|
|
initialCameraPosition: CameraPosition(
|
|
target: _deliveryPosition() ?? const LatLng(17.3850, 78.4867),
|
|
zoom: 12,
|
|
),
|
|
markers: _markers,
|
|
polylines: _polylines,
|
|
trafficEnabled: true,
|
|
zoomControlsEnabled: true,
|
|
myLocationButtonEnabled: true,
|
|
mapToolbarEnabled: false,
|
|
onMapCreated: (controller) {
|
|
_mapController = controller;
|
|
Future.delayed(const Duration(milliseconds: 400), () {
|
|
_fitMap();
|
|
});
|
|
},
|
|
),
|
|
Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: _buildBottomCard(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |