import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:customer/models/vendor_model.dart'; import 'package:customer/widget/geoflutterfire/src/geoflutterfire.dart'; import 'package:dropdown_textfield/dropdown_textfield.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart' as latlong; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import '../constant/constant.dart'; import '../models/parcel_category.dart'; import '../models/parcel_order_model.dart'; import '../models/parcel_weight_model.dart'; import '../models/user_model.dart'; import '../screen_ui/parcel_service/parcel_order_confirmation.dart'; import '../service/fire_store_utils.dart'; import '../themes/show_toast_dialog.dart'; class BookParcelController extends GetxController { // Sender details final Rx senderLocationController = TextEditingController().obs; final Rx senderNameController = TextEditingController().obs; final Rx senderMobileController = TextEditingController().obs; final Rx senderWeightController = SingleValueDropDownController().obs; final Rx senderNoteController = TextEditingController().obs; final Rx senderCountryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs; // Receiver details final Rx receiverLocationController = TextEditingController().obs; final Rx receiverNameController = TextEditingController().obs; final Rx receiverMobileController = TextEditingController().obs; final Rx receiverNoteController = TextEditingController().obs; final Rx receiverCountryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs; // Delivery type final RxString selectedDeliveryType = 'now'.obs; // Scheduled delivery fields final Rx scheduledDateController = TextEditingController().obs; final Rx scheduledTimeController = TextEditingController().obs; final RxString scheduledDate = ''.obs; final RxString scheduledTime = ''.obs; // Parcel weight list final RxList parcelWeight = [].obs; final RxList images = [].obs; final ImagePicker _picker = ImagePicker(); Rx senderLocation = Rx(null); Rx receiverLocation = Rx(null); ParcelWeightModel? selectedWeight; ParcelCategory? selectedCategory; // UI observables RxBool isScheduled = false.obs; RxDouble distance = 0.0.obs; RxDouble duration = 0.0.obs; RxDouble subTotal = 0.0.obs; @override void onInit() { super.onInit(); setArguments(); getParcelWeight(); setCurrentLocationForSenderAndReceiver(); } void setArguments() { if (Get.arguments != null && Get.arguments['parcelCategory'] != null) { selectedCategory = Get.arguments['parcelCategory']; } } Future getParcelWeight() async { parcelWeight.value = await FireStoreUtils.getParcelWeight(); } Future pickScheduledDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)), ); if (picked != null) { final formattedDate = "${picked.day}/${picked.month}/${picked.year}"; scheduledDate.value = formattedDate; scheduledDateController.value.text = formattedDate; } } Future pickScheduledTime(BuildContext context) async { final TimeOfDay? picked = await showTimePicker( context: context, initialTime: TimeOfDay.now(), ); if (picked != null) { final formattedTime = picked.format(context); scheduledTime.value = formattedTime; scheduledTimeController.value.text = formattedTime; } } void onCameraClick(BuildContext context) { final action = CupertinoActionSheet( message: Text( 'Add your parcel image.'.tr, style: const TextStyle(fontSize: 15.0), ), actions: [ CupertinoActionSheetAction( child: Text('Choose image from gallery'.tr), onPressed: () async { Navigator.pop(context); final imageList = await _picker.pickMultiImage(); if (imageList.isNotEmpty) { images.addAll(imageList); } }, ), CupertinoActionSheetAction( child: Text('Take a picture'.tr), onPressed: () async { Navigator.pop(context); final XFile? photo = await _picker.pickImage( source: ImageSource.camera, ); if (photo != null) { images.add(photo); } }, ), ], cancelButton: CupertinoActionSheetAction( child: Text('Cancel'.tr), onPressed: () => Navigator.pop(context), ), ); showCupertinoModalPopup(context: context, builder: (context) => action); } Future setCurrentLocationForSenderAndReceiver() async { try { await Geolocator.requestPermission(); final position = await Geolocator.getCurrentPosition(); final placemarks = await placemarkFromCoordinates( position.latitude, position.longitude, ); final place = placemarks.first; final address = "${place.name}, ${place.subLocality}, ${place.locality}, ${place.administrativeArea}, ${place.postalCode}, ${place.country}"; final userLocation = UserLocation( latitude: position.latitude, longitude: position.longitude, ); senderLocation.value = userLocation; senderLocationController.value.text = address; } catch (e) { debugPrint("Failed to fetch current location: $e"); } } bool validateFields() { if (senderNameController.value.text.isEmpty) { ShowToastDialog.showToast("Please enter sender name".tr); return false; } else if (senderMobileController.value.text.isEmpty) { ShowToastDialog.showToast("Please enter sender mobile".tr); return false; } else if (senderLocationController.value.text.isEmpty) { ShowToastDialog.showToast("Please enter sender address".tr); return false; } else if (receiverNameController.value.text.isEmpty) { ShowToastDialog.showToast("Please enter receiver name".tr); return false; } else if (receiverMobileController.value.text.isEmpty) { ShowToastDialog.showToast("Please enter receiver mobile".tr); return false; } else if (receiverLocationController.value.text.isEmpty) { ShowToastDialog.showToast("Please enter receiver address".tr); return false; } else if (isScheduled.value) { if (scheduledDate.value.isEmpty) { ShowToastDialog.showToast("Please select scheduled date".tr); return false; } else if (scheduledTime.value.isEmpty) { ShowToastDialog.showToast("Please select scheduled time".tr); return false; } } if (selectedWeight == null) { ShowToastDialog.showToast("Please select parcel weight".tr); return false; } else if (senderLocation.value == null || receiverLocation.value == null) { ShowToastDialog.showToast( "Please select both sender and receiver locations".tr, ); return false; } return true; } Future bookNow() async { if (!validateFields()) return; try { distance.value = 0.0; if (Constant.selectedMapType == 'osm') { print("Fetching route using OSM"); print( "Sender Location: ${senderLocation.value?.latitude}, ${senderLocation.value?.longitude}", ); print( "Receiver Location: ${receiverLocation.value?.latitude}, ${receiverLocation.value?.longitude}", ); await fetchRouteWithWaypoints([ latlong.LatLng( senderLocation.value?.latitude ?? 0.0, senderLocation.value?.longitude ?? 0.0, ), latlong.LatLng( receiverLocation.value?.latitude ?? 0.0, receiverLocation.value?.longitude ?? 0.0, ), ]); } else { await fetchGoogleRouteWithWaypoints(); } if (distance.value < 0.5) { ShowToastDialog.showToast( "Sender's location to receiver's location should be more than 1 km." .tr, ); return; } subTotal.value = (distance.value * double.parse(selectedWeight!.deliveryCharge.toString())); goToCart(); } catch (e) { ShowToastDialog.showToast("Something went wrong while booking.".tr); debugPrint("bookNow error: $e"); } } void goToCart() { DateTime senderPickup = isScheduled.value ? parseScheduledDateTime(scheduledDate.value, scheduledTime.value) : DateTime.now(); print("Sender Pickup: $distance"); ParcelOrderModel order = ParcelOrderModel( id: Constant.getUuid(), subTotal: subTotal.value.toString(), parcelType: selectedCategory?.title ?? '', parcelCategoryID: selectedCategory?.id ?? '', note: senderNoteController.value.text, receiverNote: receiverNoteController.value.text, distance: distance.value.toStringAsFixed(4), parcelWeight: selectedWeight?.title ?? '', parcelWeightCharge: selectedWeight?.deliveryCharge, sendToDriver: isScheduled.value == true ? false : true, senderPickupDateTime: Timestamp.fromDate(senderPickup), receiverPickupDateTime: Timestamp.fromDate(DateTime.now()), taxSetting: Constant.taxList, isSchedule: isScheduled.value, sourcePoint: G( geopoint: GeoPoint( senderLocation.value!.latitude ?? 0.0, senderLocation.value!.longitude ?? 0.0, ), geohash: Geoflutterfire() .point( latitude: senderLocation.value!.latitude ?? 0.0, longitude: senderLocation.value!.longitude ?? 0.0, ) .hash, ), destinationPoint: G( geopoint: GeoPoint( receiverLocation.value!.latitude ?? 0.0, receiverLocation.value!.longitude ?? 0.0, ), geohash: Geoflutterfire() .point( latitude: receiverLocation.value!.latitude ?? 0.0, longitude: receiverLocation.value!.longitude ?? 0.0, ) .hash, ), sender: LocationInformation( address: senderLocationController.value.text, name: senderNameController.value.text, phone: "(${senderCountryCodeController.value.text}) ${senderMobileController.value.text}", ), receiver: LocationInformation( address: receiverLocationController.value.text, name: receiverNameController.value.text, phone: "(${receiverCountryCodeController.value.text}) ${receiverMobileController.value.text}", ), receiverLatLong: receiverLocation.value, senderLatLong: senderLocation.value, sectionId: Constant.sectionConstantModel?.id ?? '', ); debugPrint("Order Distance: ${distance.value}"); debugPrint("Subtotal: ${subTotal.value}"); debugPrint("Order JSON: ${order.toJson()}"); Get.to( () => ParcelOrderConfirmationScreen(), arguments: {'parcelOrder': order, 'images': images}, ); } DateTime parseScheduledDateTime(String dateStr, String timeStr) { try { final dateParts = dateStr.split('/'); final day = int.parse(dateParts[0]); final month = int.parse(dateParts[1]); final year = int.parse(dateParts[2]); final time = TimeOfDay( hour: int.parse(timeStr.split(':')[0]), minute: int.parse(timeStr.split(':')[1].split(' ')[0]), ); final isPM = timeStr.toLowerCase().contains('pm'); final hour24 = isPM && time.hour < 12 ? time.hour + 12 : time.hour; return DateTime(year, month, day, hour24, time.minute); } catch (e) { debugPrint("Failed to parse scheduled date/time: $e"); return DateTime.now(); } } Future fetchGoogleRouteWithWaypoints() async { final origin = '${senderLocation.value!.latitude},${senderLocation.value!.longitude}'; final destination = '${receiverLocation.value!.latitude},${receiverLocation.value!.longitude}'; final url = Uri.parse( 'https://maps.googleapis.com/maps/api/directions/json?origin=$origin&destination=$destination&mode=driving&key=${Constant.mapAPIKey}', ); try { final response = await http.get(url); final data = json.decode(response.body); if (data['status'] == 'OK') { final route = data['routes'][0]; final legs = route['legs'] as List; num totalDistance = 0; num totalDuration = 0; for (var leg in legs) { totalDistance += leg['distance']['value']; totalDuration += leg['duration']['value']; } if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { distance.value = totalDistance / 1000.0; } else { distance.value = totalDistance / 1609.34; } duration.value = (totalDuration / 60).round().toDouble(); } else { debugPrint('Google Directions API Error: ${data['status']}'); } } catch (e) { debugPrint("Google route fetch error: $e"); } } Future fetchRouteWithWaypoints(List points) async { final coordinates = points .map((p) => '${p.longitude},${p.latitude}') .join(';'); final url = Uri.parse( 'https://router.project-osrm.org/route/v1/driving/$coordinates?overview=full&geometries=geojson', ); try { final response = await http.get(url); if (response.statusCode == 200) { final decoded = json.decode(response.body); final dist = decoded['routes'][0]['distance']; final dur = decoded['routes'][0]['duration']; if (Constant.distanceType.toLowerCase() == "KM".toLowerCase()) { distance.value = dist / 1000.00; } else { distance.value = dist / 1609.34; } duration.value = (dur / 60).round().toDouble(); } else { debugPrint("Failed to get route: ${response.body}"); } } catch (e) { debugPrint("Route fetch error: $e"); } } }