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

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