import 'dart:convert'; import 'package:Intaleq/constant/box_name.dart'; import 'package:Intaleq/constant/colors.dart'; import 'package:Intaleq/constant/links.dart'; import 'package:Intaleq/controller/functions/crud.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:get/get.dart'; import 'package:share_plus/share_plus.dart'; import '../../../main.dart'; import '../../../print.dart'; import '../../../views/widgets/error_snakbar.dart'; import '../../../views/widgets/mydialoug.dart'; import '../../functions/launch.dart'; import '../../notification/notification_captain_controller.dart'; import '../../payment/payment_controller.dart'; class InviteController extends GetxController { final TextEditingController invitePhoneController = TextEditingController(); List driverInvitationData = []; List driverInvitationDataToPassengers = []; String? couponCode; String? driverCouponCode; int selectedTab = 0; PassengerStats passengerStats = PassengerStats(); List contacts = []; RxList> contactMaps = >[].obs; @override void onInit() { super.onInit(); // It's good practice to fetch initial data in onInit or onReady // fetchDriverStats(); // fetchDriverStatsPassengers(); } void updateSelectedTab(int index) { selectedTab = index; update(); } // --- Sharing Methods --- Future shareDriverCode() async { if (driverCouponCode != null) { final String shareText = ''' ${'Join Intaleq as a driver using my referral code!'.tr} ${'Use code:'.tr} $driverCouponCode ${'Download the Intaleq Driver app now and earn rewards!'.tr} '''; await Share.share(shareText); } } Future sharePassengerCode() async { if (couponCode != null) { final String shareText = ''' ${'Get a discount on your first Intaleq ride!'.tr} ${'Use my referral code:'.tr} $couponCode ${'Download the Intaleq app now and enjoy your ride!'.tr} '''; await Share.share(shareText); } } // --- Data Fetching --- void fetchDriverStats() async { try { var response = await CRUD().get(link: AppLink.getInviteDriver, payload: { "driverId": box.read(BoxName.driverID), }); if (response != 'failure') { var data = jsonDecode(response); driverInvitationData = data['message']; update(); } } catch (e) { Log.print("Error fetching driver stats: $e"); } } void fetchDriverStatsPassengers() async { try { var response = await CRUD() .get(link: AppLink.getDriverInvitationToPassengers, payload: { "driverId": box.read(BoxName.passengerID), }); if (response != 'failure') { var data = jsonDecode(response); driverInvitationDataToPassengers = data['message']; update(); } } catch (e) { Log.print("Error fetching passenger stats: $e"); } } // --- Contact Handling --- /// **IMPROVEMENT**: This function now filters out contacts without any phone numbers. /// This is the fix for the `RangeError` you were seeing, which happened when the UI /// tried to access the first phone number of a contact that had none. Future pickContacts() async { try { if (await FlutterContacts.requestPermission(readonly: true)) { final List allContacts = await FlutterContacts.getContacts(withProperties: true); final int totalContactsOnDevice = allContacts.length; // **FIX**: Filter contacts to only include those with at least one phone number. contacts = allContacts.where((c) => c.phones.isNotEmpty).toList(); final int contactsWithPhones = contacts.length; if (contactsWithPhones > 0) { Log.print('Found $contactsWithPhones contacts with phone numbers.'); contactMaps.value = contacts.map((contact) { return { 'name': contact.displayName, 'phones': contact.phones.map((p) => p.number).toList(), 'emails': contact.emails.map((e) => e.address).toList(), }; }).toList(); update(); // **IMPROVEMENT**: Provide feedback if some contacts were filtered out. if (contactsWithPhones < totalContactsOnDevice) { Get.snackbar('Contacts Loaded'.tr, '${'Showing'.tr} $contactsWithPhones ${'of'.tr} $totalContactsOnDevice ${'contacts. Others were hidden because they don\'t have a phone number.'.tr}', snackPosition: SnackPosition.BOTTOM); } } else { Get.snackbar('No contacts found'.tr, 'No contacts with phone numbers were found on your device.'.tr); } } else { Get.snackbar('Permission denied'.tr, 'Contact permission is required to pick contacts'.tr); } } catch (e) { Log.print('Error picking contacts: $e'); Get.snackbar( 'Error'.tr, 'An error occurred while picking contacts: $e'.tr); } } void selectPhone(String phone) { invitePhoneController.text = phone; update(); Get.back(); } /// **IMPROVEMENT**: A new robust function to format phone numbers specifically for Syria (+963). /// It handles various user inputs gracefully to produce a standardized international format. String _formatSyrianPhoneNumber(String phone) { // 1. Remove all non-digit characters to clean the input. String digitsOnly = phone.replaceAll(RegExp(r'\D'), ''); // 2. If it already starts with the country code, we assume it's correct. if (digitsOnly.startsWith('963')) { return '$digitsOnly'; } // 3. If it starts with '09' (common local format), remove the leading '0'. if (digitsOnly.startsWith('09')) { digitsOnly = digitsOnly.substring(1); } // 4. Prepend the Syrian country code. return '963$digitsOnly'; } /// **IMPROVEMENT**: This method now uses the new phone formatting logic and /// sends a much-improved, user-friendly WhatsApp message. void sendInviteToPassenger() async { if (invitePhoneController.text.isEmpty || invitePhoneController.text.length < 9) { mySnackeBarError('Please enter a correct phone'.tr); return; } try { // Use the new formatting function to ensure the number is correct. String formattedPhoneNumber = _formatSyrianPhoneNumber(invitePhoneController.text); var response = await CRUD().post(link: AppLink.addInvitationPassenger, payload: { "driverId": box.read(BoxName.passengerID), "inviterPassengerPhone": formattedPhoneNumber, }); if (response != 'failure') { var d = response; Get.snackbar('Success'.tr, 'Invite sent successfully'.tr, backgroundColor: Colors.green, colorText: Colors.white); String expirationTime = d['message']['expirationTime'].toString(); String inviteCode = d['message']['inviteCode'].toString(); // New and improved WhatsApp message for better user engagement. String message = "👋 ${'Hello! I\'m inviting you to try Intaleq.'.tr}\n\n" "🎁 ${'Use my invitation code to get a special gift on your first ride!'.tr}\n\n" "${'Your personal invitation code is:'.tr}\n" "*$inviteCode*\n\n" "⏳ ${'Be sure to use it quickly! This code expires at'.tr} *$expirationTime*.\n\n" "📲 ${'Download the app now:'.tr}\n" "• *Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n" "• *iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179\n\n" "${'See you on the road!'.tr} 🚗"; launchCommunication('whatsapp', formattedPhoneNumber, message); invitePhoneController.clear(); update(); } else { Get.snackbar( 'Error'.tr, "This phone number has already been invited.".tr, backgroundColor: AppColor.redColor, duration: const Duration(seconds: 4)); } } catch (e) { Log.print("Error sending invite: $e"); Get.snackbar( 'Error'.tr, 'An unexpected error occurred. Please try again.'.tr, backgroundColor: AppColor.redColor); } } // This function is dependent on the `pickContacts` method filtering out contacts without phones. savePhoneToServer() async { for (var contactMap in contactMaps) { // The `pickContacts` function ensures the 'phones' list is not empty here. var phones = contactMap['phones'] as List; var res = await CRUD().post(link: AppLink.savePhones, payload: { "name": contactMap['name'] ?? 'No Name', "phones": phones.first, // Safely access the first phone number "phones2": phones.join(', '), }); if (res == 'failure') { Log.print('Failed to save contact: ${contactMap['name']}'); } } } void onSelectPassengerInvitation(int index) async { try { final invitation = driverInvitationDataToPassengers[index]; final tripCount = int.tryParse(invitation['countOfInvitDriver'].toString()) ?? 0; final passengerName = invitation['passengerName'].toString(); final isGiftTaken = invitation['isGiftToken'].toString() == '1'; if (tripCount >= 2) { // Gift can be claimed if (!isGiftTaken) { MyDialog().getDialog( 'You deserve the gift'.tr, '${'Claim your 20 LE gift for inviting'.tr} $passengerName!', () async { Get.back(); // Close dialog first await Get.find().addPassengersWallet('20'); await CRUD().post( link: AppLink.updatePassengerGift, payload: {'id': invitation['id']}, ); NotificationCaptainController().addNotificationCaptain( invitation['passengerInviterId'].toString(), "You have got a gift for invitation".tr, '${"You have earned 20".tr} ${'LE'}', false, ); fetchDriverStatsPassengers(); // Refresh list }, ); } else { MyDialog().getDialog( "Gift Already Claimed".tr, "You have already received your gift for inviting $passengerName." .tr, () => Get.back(), ); } } else { // Gift not yet earned MyDialog().getDialog( '${'Keep it up!'.tr}', '$passengerName ${'has completed'.tr} $tripCount / 2 ${'trips'.tr}. ${"You can claim your gift once they complete 2 trips.".tr}', () => Get.back(), ); } } catch (e) { Log.print("Error in onSelectPassengerInvitation: $e"); } } } class PassengerStats { final int totalInvites; final int activeUsers; final double totalEarnings; PassengerStats({ this.totalInvites = 0, this.activeUsers = 0, this.totalEarnings = 0.0, }); }