432 lines
15 KiB
Dart
432 lines
15 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:easy_localization/easy_localization.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' hide Trans;
|
|
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");
|
|
}
|
|
}
|
|
}
|