341 lines
14 KiB
Dart
341 lines
14 KiB
Dart
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<TextEditingController> senderLocationController = TextEditingController().obs;
|
|
final Rx<TextEditingController> senderNameController = TextEditingController().obs;
|
|
final Rx<TextEditingController> senderMobileController = TextEditingController().obs;
|
|
final Rx<SingleValueDropDownController> senderWeightController = SingleValueDropDownController().obs;
|
|
final Rx<TextEditingController> senderNoteController = TextEditingController().obs;
|
|
final Rx<TextEditingController> senderCountryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs;
|
|
|
|
// Receiver details
|
|
final Rx<TextEditingController> receiverLocationController = TextEditingController().obs;
|
|
final Rx<TextEditingController> receiverNameController = TextEditingController().obs;
|
|
final Rx<TextEditingController> receiverMobileController = TextEditingController().obs;
|
|
final Rx<TextEditingController> receiverNoteController = TextEditingController().obs;
|
|
final Rx<TextEditingController> receiverCountryCodeController = TextEditingController(text: Constant.defaultCountryCode).obs;
|
|
|
|
// Delivery type
|
|
final RxString selectedDeliveryType = 'now'.obs;
|
|
|
|
// Scheduled delivery fields
|
|
final Rx<TextEditingController> scheduledDateController = TextEditingController().obs;
|
|
final Rx<TextEditingController> scheduledTimeController = TextEditingController().obs;
|
|
final RxString scheduledDate = ''.obs;
|
|
final RxString scheduledTime = ''.obs;
|
|
|
|
// Parcel weight list
|
|
final RxList<ParcelWeightModel> parcelWeight = <ParcelWeightModel>[].obs;
|
|
|
|
final RxList<XFile> images = <XFile>[].obs;
|
|
final ImagePicker _picker = ImagePicker();
|
|
|
|
Rx<UserLocation?> senderLocation = Rx<UserLocation?>(null);
|
|
Rx<UserLocation?> receiverLocation = Rx<UserLocation?>(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<void> getParcelWeight() async {
|
|
parcelWeight.value = await FireStoreUtils.getParcelWeight();
|
|
}
|
|
|
|
Future<void> 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<void> 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: <Widget>[
|
|
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<void> 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<void> 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<void> 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<void> fetchRouteWithWaypoints(List<latlong.LatLng> 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");
|
|
}
|
|
}
|
|
}
|