Files
intaleq/lib/controller/home/profile/invit_controller.dart
Hamza-Ayed 11dfe94bbb 25-12-1/1
2025-12-01 07:53:52 +03:00

313 lines
11 KiB
Dart

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<Contact> contacts = <Contact>[];
RxList<Map<String, dynamic>> contactMaps = <Map<String, dynamic>>[].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<void> 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<void> 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<void> pickContacts() async {
try {
if (await FlutterContacts.requestPermission(readonly: true)) {
final List<Contact> 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<String>;
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<PaymentController>().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,
});
}