Merge pull request 'feat: Add contact picking functionality in employee form and update dependencies' (#45) from Vaibhav_Enhancement-#454 into main

Reviewed-on: #45
This commit is contained in:
vaibhav.surve 2025-06-04 10:44:11 +00:00
commit 3926e762e5
4 changed files with 174 additions and 31 deletions

View File

@ -1,6 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<application <application
android:label="marco" android:label="marco"

View File

@ -5,6 +5,8 @@ import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart';
enum Gender { enum Gender {
male, male,
@ -18,7 +20,7 @@ final Logger logger = Logger();
class AddEmployeeController extends MyController { class AddEmployeeController extends MyController {
List<PlatformFile> files = []; List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
Gender? selectedGender; Gender? selectedGender;
List<Map<String, dynamic>> roles = []; List<Map<String, dynamic>> roles = [];
String? selectedRoleId; String? selectedRoleId;
@ -59,11 +61,18 @@ class AddEmployeeController extends MyController {
}; };
String selectedCountryCode = "+91"; String selectedCountryCode = "+91";
bool showOnline = true;
final List<String> categories = [];
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
logger.i("Initializing AddEmployeeController..."); logger.i("Initializing AddEmployeeController...");
_initializeFields();
fetchRoles(); fetchRoles();
}
void _initializeFields() {
basicValidator.addField( basicValidator.addField(
'first_name', 'first_name',
label: "First Name", label: "First Name",
@ -85,10 +94,6 @@ class AddEmployeeController extends MyController {
logger.i("Fields initialized for first_name, phone_number, last_name."); logger.i("Fields initialized for first_name, phone_number, last_name.");
} }
bool showOnline = true;
final List<String> categories = [];
void onGenderSelected(Gender? gender) { void onGenderSelected(Gender? gender) {
selectedGender = gender; selectedGender = gender;
logger.i("Gender selected: ${gender?.name}"); logger.i("Gender selected: ${gender?.name}");
@ -97,13 +102,17 @@ class AddEmployeeController extends MyController {
Future<void> fetchRoles() async { Future<void> fetchRoles() async {
logger.i("Fetching roles..."); logger.i("Fetching roles...");
final result = await ApiService.getRoles(); try {
if (result != null) { final result = await ApiService.getRoles();
roles = List<Map<String, dynamic>>.from(result); if (result != null) {
logger.i("Roles fetched successfully."); roles = List<Map<String, dynamic>>.from(result);
update(); logger.i("Roles fetched successfully.");
} else { update();
logger.e("Failed to fetch roles."); } else {
logger.e("Failed to fetch roles: null result");
}
} catch (e, st) {
logger.e("Error fetching roles: $e", error: e, stackTrace: st);
} }
} }
@ -133,30 +142,149 @@ class AddEmployeeController extends MyController {
logger.i( logger.i(
"Creating employee with Name: $firstName $lastName, Phone: $phoneNumber, Gender: ${selectedGender!.name}"); "Creating employee with Name: $firstName $lastName, Phone: $phoneNumber, Gender: ${selectedGender!.name}");
final response = await ApiService.createEmployee( try {
firstName: firstName!, final response = await ApiService.createEmployee(
lastName: lastName!, firstName: firstName!,
phoneNumber: phoneNumber!, lastName: lastName!,
gender: selectedGender!.name, phoneNumber: phoneNumber!,
jobRoleId: selectedRoleId!, gender: selectedGender!.name,
); jobRoleId: selectedRoleId!,
if (response == true) {
logger.i("Employee created successfully.");
showAppSnackbar(
title: "Success",
message: "Employee created successfully!",
type: SnackbarType.success,
); );
if (response == true) {
logger.i("Employee created successfully.");
showAppSnackbar(
title: "Success",
message: "Employee created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to create employee (response false).");
}
} catch (e, st) {
logger.e("Error creating employee: $e", error: e, stackTrace: st);
}
showAppSnackbar(
title: "Error",
message: "Failed to create employee.",
type: SnackbarType.error,
);
return false;
}
Future<bool> _checkAndRequestContactsPermission() async {
final status = await Permission.contacts.request();
if (status.isGranted) {
return true; return true;
} else { }
logger.e("Failed to create employee.");
if (status.isPermanentlyDenied) {
await openAppSettings();
}
showAppSnackbar(
title: "Permission Required",
message:
"Please allow Contacts permission from settings to pick a contact.",
type: SnackbarType.warning,
);
return false;
}
Future<void> pickContact(BuildContext context) async {
final permissionGranted = await _checkAndRequestContactsPermission();
if (!permissionGranted) return;
try {
final picked = await FlutterContacts.openExternalPick();
if (picked == null) return; // User canceled contact picking
final contact =
await FlutterContacts.getContact(picked.id, withProperties: true);
if (contact == null) {
showAppSnackbar(
title: "Error",
message: "Failed to load contact details.",
type: SnackbarType.error,
);
return;
}
if (contact.phones.isEmpty) {
showAppSnackbar(
title: "No Phone Number",
message: "Selected contact has no phone number.",
type: SnackbarType.warning,
);
return;
}
final indiaPhones = contact.phones.where((p) {
final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), '');
return normalized.startsWith('+91') ||
RegExp(r'^\d{10}$').hasMatch(normalized);
}).toList();
if (indiaPhones.isEmpty) {
showAppSnackbar(
title: "No Indian Number",
message: "Selected contact has no Indian (+91) phone number.",
type: SnackbarType.warning,
);
return;
}
String? selectedPhone;
if (indiaPhones.length == 1) {
selectedPhone = indiaPhones.first.number;
} else {
selectedPhone = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: Text("Choose an Indian number"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: indiaPhones
.map((p) => ListTile(
title: Text(p.number),
onTap: () => Navigator.of(ctx).pop(p.number),
))
.toList(),
),
),
);
if (selectedPhone == null) return;
}
final normalizedPhone = selectedPhone.replaceAll(RegExp(r'[^0-9]'), '');
// Remove country code prefix if present, keep only 10 digits
String phoneWithoutCountryCode;
if (normalizedPhone.length > 10) {
phoneWithoutCountryCode =
normalizedPhone.substring(normalizedPhone.length - 10);
} else {
phoneWithoutCountryCode = normalizedPhone;
}
basicValidator.getController('phone_number')?.text =
phoneWithoutCountryCode;
update();
} catch (e, st) {
logger.e("Error fetching contacts: $e", error: e, stackTrace: st);
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Failed to create employee.", message: "Failed to fetch contacts.",
type: SnackbarType.error, type: SnackbarType.error,
); );
return false;
} }
} }
} }

View File

@ -214,7 +214,17 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
}, },
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
decoration: decoration:
_inputDecoration("eg: 9876543210"), _inputDecoration("eg: 9876543210")
.copyWith(
suffixIcon: IconButton(
icon: Icon(Icons.contacts),
tooltip: "Pick from contacts",
onPressed: () async {
await controller
.pickContact(context);
},
),
),
), ),
], ],
), ),

View File

@ -68,6 +68,7 @@ dependencies:
path_provider: ^2.1.2 path_provider: ^2.1.2
path: ^1.9.0 path: ^1.9.0
percent_indicator: ^4.2.2 percent_indicator: ^4.2.2
flutter_contacts: ^1.1.9+2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter