Vaibhav_Feature-#768 #59
@ -8,13 +8,13 @@ import 'package:geocoding/geocoding.dart';
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
|
import 'package:marco/model/employee_model.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
import 'package:marco/model/employee_model.dart';
|
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
|
|
||||||
@ -50,6 +50,9 @@ class AddExpenseController extends GetxController {
|
|||||||
final paymentModes = <PaymentModeModel>[].obs;
|
final paymentModes = <PaymentModeModel>[].obs;
|
||||||
final allEmployees = <EmployeeModel>[].obs;
|
final allEmployees = <EmployeeModel>[].obs;
|
||||||
final existingAttachments = <Map<String, dynamic>>[].obs;
|
final existingAttachments = <Map<String, dynamic>>[].obs;
|
||||||
|
final employeeSearchController = TextEditingController();
|
||||||
|
final isSearchingEmployees = false.obs;
|
||||||
|
final employeeSearchResults = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
// Editing
|
// Editing
|
||||||
String? editingExpenseId;
|
String? editingExpenseId;
|
||||||
@ -61,7 +64,10 @@ class AddExpenseController extends GetxController {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
fetchMasterData();
|
fetchMasterData();
|
||||||
fetchGlobalProjects();
|
fetchGlobalProjects();
|
||||||
fetchAllEmployees();
|
|
||||||
|
employeeSearchController.addListener(() {
|
||||||
|
searchEmployees(employeeSearchController.text);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -77,12 +83,44 @@ class AddExpenseController extends GetxController {
|
|||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> searchEmployees(String searchQuery) async {
|
||||||
|
if (searchQuery.trim().isEmpty) {
|
||||||
|
employeeSearchResults.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearchingEmployees.value = true;
|
||||||
|
try {
|
||||||
|
final results = await ApiService.searchEmployeesBasic(
|
||||||
|
searchString: searchQuery.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (results != null) {
|
||||||
|
employeeSearchResults.assignAll(
|
||||||
|
results.map((e) => EmployeeModel.fromJson(e)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
employeeSearchResults.clear();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("Error searching employees: $e", level: LogLevel.error);
|
||||||
|
employeeSearchResults.clear();
|
||||||
|
} finally {
|
||||||
|
isSearchingEmployees.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Form Population for Edit ----------
|
// ---------- Form Population for Edit ----------
|
||||||
void populateFieldsForEdit(Map<String, dynamic> data) {
|
Future<void> populateFieldsForEdit(Map<String, dynamic> data) async {
|
||||||
isEditMode.value = true;
|
isEditMode.value = true;
|
||||||
editingExpenseId = data['id'];
|
editingExpenseId = data['id'];
|
||||||
|
|
||||||
// Basic fields
|
// --- Fetch all Paid By variables up front ---
|
||||||
|
final paidById = (data['paidById'] ?? '').toString();
|
||||||
|
final paidByFirstName = (data['paidByFirstName'] ?? '').toString().trim();
|
||||||
|
final paidByLastName = (data['paidByLastName'] ?? '').toString().trim();
|
||||||
|
|
||||||
|
// --- Standard Fields ---
|
||||||
selectedProject.value = data['projectName'] ?? '';
|
selectedProject.value = data['projectName'] ?? '';
|
||||||
amountController.text = data['amount']?.toString() ?? '';
|
amountController.text = data['amount']?.toString() ?? '';
|
||||||
supplierController.text = data['supplerName'] ?? '';
|
supplierController.text = data['supplerName'] ?? '';
|
||||||
@ -90,7 +128,7 @@ class AddExpenseController extends GetxController {
|
|||||||
transactionIdController.text = data['transactionId'] ?? '';
|
transactionIdController.text = data['transactionId'] ?? '';
|
||||||
locationController.text = data['location'] ?? '';
|
locationController.text = data['location'] ?? '';
|
||||||
|
|
||||||
// Transaction Date
|
// --- Transaction Date ---
|
||||||
if (data['transactionDate'] != null) {
|
if (data['transactionDate'] != null) {
|
||||||
try {
|
try {
|
||||||
final parsedDate = DateTime.parse(data['transactionDate']);
|
final parsedDate = DateTime.parse(data['transactionDate']);
|
||||||
@ -107,31 +145,29 @@ class AddExpenseController extends GetxController {
|
|||||||
transactionDateController.clear();
|
transactionDateController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// No of Persons
|
// --- No of Persons ---
|
||||||
noOfPersonsController.text = (data['noOfPersons'] ?? 0).toString();
|
noOfPersonsController.text = (data['noOfPersons'] ?? 0).toString();
|
||||||
|
|
||||||
// Select Expense Type and Payment Mode by matching IDs
|
// --- Dropdown selections ---
|
||||||
selectedExpenseType.value =
|
selectedExpenseType.value =
|
||||||
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
|
expenseTypes.firstWhereOrNull((e) => e.id == data['expensesTypeId']);
|
||||||
selectedPaymentMode.value =
|
selectedPaymentMode.value =
|
||||||
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
|
paymentModes.firstWhereOrNull((e) => e.id == data['paymentModeId']);
|
||||||
|
|
||||||
// Select Paid By employee matching id (case insensitive, trimmed)
|
// --- Paid By select ---
|
||||||
final paidById = data['paidById']?.toString().trim().toLowerCase() ?? '';
|
// 1. By ID
|
||||||
selectedPaidBy.value = allEmployees
|
// --- Paid By select ---
|
||||||
.firstWhereOrNull((e) => e.id.trim().toLowerCase() == paidById);
|
selectedPaidBy.value =
|
||||||
|
allEmployees.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
||||||
|
|
||||||
if (selectedPaidBy.value == null && paidById.isNotEmpty) {
|
if (selectedPaidBy.value == null) {
|
||||||
logSafe('⚠️ Could not match paidById: "$paidById"',
|
final fullName = '$paidByFirstName $paidByLastName';
|
||||||
level: LogLevel.warning);
|
await searchEmployees(fullName);
|
||||||
for (var emp in allEmployees) {
|
selectedPaidBy.value = employeeSearchResults
|
||||||
logSafe(
|
.firstWhereOrNull((e) => e.id.trim() == paidById.trim());
|
||||||
'Employee ID: "${emp.id.trim().toLowerCase()}", Name: "${emp.name}"',
|
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate existing attachments if present
|
// --- Existing Attachments ---
|
||||||
existingAttachments.clear();
|
existingAttachments.clear();
|
||||||
if (data['attachments'] != null && data['attachments'] is List) {
|
if (data['attachments'] != null && data['attachments'] is List) {
|
||||||
existingAttachments
|
existingAttachments
|
||||||
@ -184,7 +220,6 @@ class AddExpenseController extends GetxController {
|
|||||||
await Future.wait([
|
await Future.wait([
|
||||||
fetchMasterData(),
|
fetchMasterData(),
|
||||||
fetchGlobalProjects(),
|
fetchGlobalProjects(),
|
||||||
fetchAllEmployees(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,18 +486,4 @@ class AddExpenseController extends GetxController {
|
|||||||
logSafe("Error fetching projects: $e", level: LogLevel.error);
|
logSafe("Error fetching projects: $e", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchAllEmployees() async {
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
final response = await ApiService.getAllEmployees();
|
|
||||||
if (response != null) {
|
|
||||||
allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e)));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logSafe("Error fetching employees: $e", level: LogLevel.error);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import 'package:marco/model/expense/expense_type_model.dart';
|
|||||||
import 'package:marco/model/expense/expense_status_model.dart';
|
import 'package:marco/model/expense/expense_status_model.dart';
|
||||||
import 'package:marco/model/employee_model.dart';
|
import 'package:marco/model/employee_model.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ExpenseController extends GetxController {
|
class ExpenseController extends GetxController {
|
||||||
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
|
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
|
||||||
@ -33,6 +33,10 @@ class ExpenseController extends GetxController {
|
|||||||
<EmployeeModel>[].obs;
|
<EmployeeModel>[].obs;
|
||||||
final RxString selectedDateType = 'Transaction Date'.obs;
|
final RxString selectedDateType = 'Transaction Date'.obs;
|
||||||
|
|
||||||
|
final employeeSearchController = TextEditingController();
|
||||||
|
final isSearchingEmployees = false.obs;
|
||||||
|
final employeeSearchResults = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
final List<String> dateTypes = [
|
final List<String> dateTypes = [
|
||||||
'Transaction Date',
|
'Transaction Date',
|
||||||
'Created At',
|
'Created At',
|
||||||
@ -46,6 +50,9 @@ class ExpenseController extends GetxController {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
loadInitialMasterData();
|
loadInitialMasterData();
|
||||||
fetchAllEmployees();
|
fetchAllEmployees();
|
||||||
|
employeeSearchController.addListener(() {
|
||||||
|
searchEmployees(employeeSearchController.text);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isFilterApplied {
|
bool get isFilterApplied {
|
||||||
@ -94,6 +101,33 @@ class ExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> searchEmployees(String searchQuery) async {
|
||||||
|
if (searchQuery.trim().isEmpty) {
|
||||||
|
employeeSearchResults.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearchingEmployees.value = true;
|
||||||
|
try {
|
||||||
|
final results = await ApiService.searchEmployeesBasic(
|
||||||
|
searchString: searchQuery.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (results != null) {
|
||||||
|
employeeSearchResults.assignAll(
|
||||||
|
results.map((e) => EmployeeModel.fromJson(e)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
employeeSearchResults.clear();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("Error searching employees: $e", level: LogLevel.error);
|
||||||
|
employeeSearchResults.clear();
|
||||||
|
} finally {
|
||||||
|
isSearchingEmployees.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch expenses using filters
|
/// Fetch expenses using filters
|
||||||
Future<void> fetchExpenses({
|
Future<void> fetchExpenses({
|
||||||
List<String>? projectIds,
|
List<String>? projectIds,
|
||||||
@ -188,8 +222,9 @@ class ExpenseController extends GetxController {
|
|||||||
|
|
||||||
final expenseStatusData = await ApiService.getMasterExpenseStatus();
|
final expenseStatusData = await ApiService.getMasterExpenseStatus();
|
||||||
if (expenseStatusData is List) {
|
if (expenseStatusData is List) {
|
||||||
expenseStatuses.value =
|
expenseStatuses.value = expenseStatusData
|
||||||
expenseStatusData.map((e) => ExpenseStatusModel.fromJson(e)).toList();
|
.map((e) => ExpenseStatusModel.fromJson(e))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
@ -198,7 +233,7 @@ class ExpenseController extends GetxController {
|
|||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch global projects
|
/// Fetch global projects
|
||||||
Future<void> fetchGlobalProjects() async {
|
Future<void> fetchGlobalProjects() async {
|
||||||
|
@ -17,6 +17,7 @@ class ApiEndpoints {
|
|||||||
// Employee Screen API Endpoints
|
// Employee Screen API Endpoints
|
||||||
static const String getAllEmployeesByProject = "/employee/list";
|
static const String getAllEmployeesByProject = "/employee/list";
|
||||||
static const String getAllEmployees = "/employee/list";
|
static const String getAllEmployees = "/employee/list";
|
||||||
|
static const String getEmployeesWithoutPermission = "/employee/basic";
|
||||||
static const String getRoles = "/roles/jobrole";
|
static const String getRoles = "/roles/jobrole";
|
||||||
static const String createEmployee = "/employee/manage-mobile";
|
static const String createEmployee = "/employee/manage-mobile";
|
||||||
static const String getEmployeeInfo = "/employee/profile/get";
|
static const String getEmployeeInfo = "/employee/profile/get";
|
||||||
|
@ -1151,6 +1151,31 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === Employee APIs ===
|
// === Employee APIs ===
|
||||||
|
/// Search employees by first name and last name only (not middle name)
|
||||||
|
/// Returns a list of up to 10 employee records matching the search string.
|
||||||
|
static Future<List<dynamic>?> searchEmployeesBasic({
|
||||||
|
String? searchString,
|
||||||
|
}) async {
|
||||||
|
// Remove ArgumentError check because searchString is optional now
|
||||||
|
|
||||||
|
final queryParams = <String, String>{};
|
||||||
|
|
||||||
|
// Add searchString to query parameters only if it's not null or empty
|
||||||
|
if (searchString != null && searchString.isNotEmpty) {
|
||||||
|
queryParams['searchString'] = searchString;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _getRequest(
|
||||||
|
ApiEndpoints.getEmployeesWithoutPermission,
|
||||||
|
queryParams: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
return _parseResponse(response, label: 'Search Employees Basic');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<dynamic>?> getAllEmployeesByProject(
|
static Future<List<dynamic>?> getAllEmployeesByProject(
|
||||||
String projectId) async {
|
String projectId) async {
|
||||||
|
30
lib/model/employees/employee_with_id_name_model.dart
Normal file
30
lib/model/employees/employee_with_id_name_model.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
class EmployeeModelWithIdName {
|
||||||
|
final String id;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
EmployeeModelWithIdName({
|
||||||
|
required this.id,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory EmployeeModelWithIdName.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EmployeeModelWithIdName(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
firstName: json['firstName'] ?? '',
|
||||||
|
lastName: json['lastName'] ?? '',
|
||||||
|
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'firstName': name.split(' ').first,
|
||||||
|
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:marco/controller/expense/add_expense_controller.dart';
|
import 'package:marco/controller/expense/add_expense_controller.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
|
import 'package:marco/model/expense/employee_selector_bottom_sheet.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -39,34 +40,23 @@ class _AddExpenseBottomSheet extends StatefulWidget {
|
|||||||
class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
||||||
final AddExpenseController controller = Get.put(AddExpenseController());
|
final AddExpenseController controller = Get.put(AddExpenseController());
|
||||||
|
|
||||||
void _showEmployeeList() {
|
void _showEmployeeList() async {
|
||||||
showModalBottomSheet(
|
await showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Colors.white,
|
isScrollControlled: true,
|
||||||
|
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
builder: (_) => Obx(() {
|
|
||||||
final employees = controller.allEmployees;
|
|
||||||
return SizedBox(
|
|
||||||
height: 300,
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: employees.length,
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final emp = employees[index];
|
|
||||||
final fullName = '${emp.firstName} ${emp.lastName}'.trim();
|
|
||||||
return ListTile(
|
|
||||||
title: Text(fullName.isNotEmpty ? fullName : "Unnamed"),
|
|
||||||
onTap: () {
|
|
||||||
controller.selectedPaidBy.value = emp;
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) => EmployeeSelectorBottomSheet(),
|
||||||
);
|
);
|
||||||
}),
|
|
||||||
);
|
// Optional cleanup
|
||||||
}
|
controller.employeeSearchController.clear();
|
||||||
|
controller.employeeSearchResults.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _showOptionList<T>(
|
Future<void> _showOptionList<T>(
|
||||||
List<T> options,
|
List<T> options,
|
||||||
|
73
lib/model/expense/employee_selector_bottom_sheet.dart
Normal file
73
lib/model/expense/employee_selector_bottom_sheet.dart
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/controller/expense/add_expense_controller.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
|
|
||||||
|
class EmployeeSelectorBottomSheet extends StatelessWidget {
|
||||||
|
final AddExpenseController controller = Get.find<AddExpenseController>();
|
||||||
|
|
||||||
|
EmployeeSelectorBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BaseBottomSheet(
|
||||||
|
title: "Search Employee",
|
||||||
|
onCancel: () => Get.back(),
|
||||||
|
onSubmit: () {},
|
||||||
|
showButtons: false,
|
||||||
|
child: Obx(() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: controller.employeeSearchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search by name, email...",
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
),
|
||||||
|
onChanged: (value) => controller.searchEmployees(value),
|
||||||
|
),
|
||||||
|
MySpacing.height(12),
|
||||||
|
SizedBox(
|
||||||
|
height: 400, // Adjust this if needed
|
||||||
|
child: controller.isSearchingEmployees.value
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: controller.employeeSearchResults.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: MyText.bodyMedium(
|
||||||
|
"No employees found.",
|
||||||
|
fontWeight: 500,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: controller.employeeSearchResults.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final emp = controller.employeeSearchResults[index];
|
||||||
|
final fullName =
|
||||||
|
'${emp.firstName} ${emp.lastName}'.trim();
|
||||||
|
return ListTile(
|
||||||
|
title: MyText.bodyLarge(
|
||||||
|
fullName.isNotEmpty ? fullName : "Unnamed",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
controller.selectedPaidBy.value = emp;
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
128
lib/model/expense/employee_selector_for_filter_bottom_sheet.dart
Normal file
128
lib/model/expense/employee_selector_for_filter_bottom_sheet.dart
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
|
import 'package:marco/model/employee_model.dart';
|
||||||
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
|
|
||||||
|
class EmployeeSelectorBottomSheet extends StatefulWidget {
|
||||||
|
final RxList<EmployeeModel> selectedEmployees;
|
||||||
|
final Future<List<EmployeeModel>> Function(String) searchEmployees;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const EmployeeSelectorBottomSheet({
|
||||||
|
super.key,
|
||||||
|
required this.selectedEmployees,
|
||||||
|
required this.searchEmployees,
|
||||||
|
this.title = "Select Employees",
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EmployeeSelectorBottomSheet> createState() =>
|
||||||
|
_EmployeeSelectorBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EmployeeSelectorBottomSheetState
|
||||||
|
extends State<EmployeeSelectorBottomSheet> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
final RxBool isSearching = false.obs;
|
||||||
|
final RxList<EmployeeModel> searchResults = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Initial fetch (empty text gets all/none as you wish)
|
||||||
|
_searchEmployees('');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _searchEmployees(String query) async {
|
||||||
|
isSearching.value = true;
|
||||||
|
List<EmployeeModel> results = await widget.searchEmployees(query);
|
||||||
|
searchResults.assignAll(results);
|
||||||
|
isSearching.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submitSelection() =>
|
||||||
|
Get.back(result: widget.selectedEmployees.toList());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BaseBottomSheet(
|
||||||
|
title: widget.title,
|
||||||
|
onCancel: () => Get.back(),
|
||||||
|
onSubmit: _submitSelection,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Chips
|
||||||
|
Obx(() => widget.selectedEmployees.isEmpty
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
children: widget.selectedEmployees
|
||||||
|
.map(
|
||||||
|
(emp) => Chip(
|
||||||
|
label: MyText(emp.name),
|
||||||
|
onDeleted: () =>
|
||||||
|
widget.selectedEmployees.remove(emp),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
)),
|
||||||
|
MySpacing.height(8),
|
||||||
|
|
||||||
|
// Search box
|
||||||
|
TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search Employees...",
|
||||||
|
border:
|
||||||
|
OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
onChanged: _searchEmployees,
|
||||||
|
),
|
||||||
|
MySpacing.height(12),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
height: 320, // CHANGE AS PER DESIGN!
|
||||||
|
child: Obx(() {
|
||||||
|
if (isSearching.value) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (searchResults.isEmpty) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
child:
|
||||||
|
MyText('No results', style: MyTextStyle.bodyMedium()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: searchResults.length,
|
||||||
|
separatorBuilder: (_, __) => Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final emp = searchResults[index];
|
||||||
|
final isSelected = widget.selectedEmployees.contains(emp);
|
||||||
|
return ListTile(
|
||||||
|
title: MyText(emp.name),
|
||||||
|
trailing: isSelected
|
||||||
|
? Icon(Icons.check_circle, color: Colors.indigo)
|
||||||
|
: Icon(Icons.radio_button_unchecked,
|
||||||
|
color: Colors.grey),
|
||||||
|
onTap: () {
|
||||||
|
if (isSelected) {
|
||||||
|
widget.selectedEmployees.remove(emp);
|
||||||
|
} else {
|
||||||
|
widget.selectedEmployees.add(emp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -130,6 +130,10 @@ class ExpenseDetailScreen extends StatelessWidget {
|
|||||||
'expensesTypeId': expense.expensesType.id,
|
'expensesTypeId': expense.expensesType.id,
|
||||||
'paymentModeId': expense.paymentMode.id,
|
'paymentModeId': expense.paymentMode.id,
|
||||||
'paidById': expense.paidBy.id,
|
'paidById': expense.paidBy.id,
|
||||||
|
// ==== Add these lines below ====
|
||||||
|
'paidByFirstName': expense.paidBy.firstName,
|
||||||
|
'paidByLastName': expense.paidBy.lastName,
|
||||||
|
// =================================
|
||||||
'attachments': expense.documents
|
'attachments': expense.documents
|
||||||
.map((doc) => {
|
.map((doc) => {
|
||||||
'url': doc.preSignedUrl,
|
'url': doc.preSignedUrl,
|
||||||
@ -146,7 +150,7 @@ class ExpenseDetailScreen extends StatelessWidget {
|
|||||||
await addCtrl.loadMasterData();
|
await addCtrl.loadMasterData();
|
||||||
addCtrl.populateFieldsForEdit(editData);
|
addCtrl.populateFieldsForEdit(editData);
|
||||||
|
|
||||||
await showAddExpenseBottomSheet( isEdit: true,);
|
await showAddExpenseBottomSheet(isEdit: true);
|
||||||
|
|
||||||
// Refresh expense details after editing
|
// Refresh expense details after editing
|
||||||
await controller.fetchExpenseDetails();
|
await controller.fetchExpenseDetails();
|
||||||
|
@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/model/employee_model.dart';
|
import 'package:marco/model/employee_model.dart';
|
||||||
|
import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.dart';
|
||||||
|
|
||||||
class ExpenseFilterBottomSheet extends StatelessWidget {
|
class ExpenseFilterBottomSheet extends StatelessWidget {
|
||||||
final ExpenseController expenseController;
|
final ExpenseController expenseController;
|
||||||
@ -18,9 +19,16 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIX: create search adapter
|
||||||
|
Future<List<EmployeeModel>> searchEmployeesForBottomSheet(
|
||||||
|
String query) async {
|
||||||
|
await expenseController
|
||||||
|
.searchEmployees(query); // async method, returns void
|
||||||
|
return expenseController.employeeSearchResults.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Obx rebuilds the widget when observable values from the controller change.
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
title: 'Filter Expenses',
|
title: 'Filter Expenses',
|
||||||
@ -41,11 +49,11 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => expenseController.clearFilters(),
|
onPressed: () => expenseController.clearFilters(),
|
||||||
child: const Text(
|
child: MyText(
|
||||||
"Reset Filter",
|
"Reset Filter",
|
||||||
style: TextStyle(
|
style: MyTextStyle.labelMedium(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: 600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -57,9 +65,9 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildDateRangeFilter(context),
|
_buildDateRangeFilter(context),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildPaidByFilter(),
|
_buildPaidByFilter(context),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildCreatedByFilter(),
|
_buildCreatedByFilter(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -67,7 +75,6 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a generic field layout with a label and a child widget.
|
|
||||||
Widget _buildField(String label, Widget child) {
|
Widget _buildField(String label, Widget child) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -79,7 +86,6 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracted widget builder for the Project filter.
|
|
||||||
Widget _buildProjectFilter(BuildContext context) {
|
Widget _buildProjectFilter(BuildContext context) {
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Project",
|
"Project",
|
||||||
@ -94,7 +100,6 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracted widget builder for the Expense Status filter.
|
|
||||||
Widget _buildStatusFilter(BuildContext context) {
|
Widget _buildStatusFilter(BuildContext context) {
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Expense Status",
|
"Expense Status",
|
||||||
@ -117,7 +122,6 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracted widget builder for the Date Range filter.
|
|
||||||
Widget _buildDateRangeFilter(BuildContext context) {
|
Widget _buildDateRangeFilter(BuildContext context) {
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Date Filter",
|
"Date Filter",
|
||||||
@ -130,7 +134,7 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
.map(
|
.map(
|
||||||
(type) => ButtonSegment(
|
(type) => ButtonSegment(
|
||||||
value: type,
|
value: type,
|
||||||
label: Text(
|
label: MyText(
|
||||||
type,
|
type,
|
||||||
style: MyTextStyle.bodySmall(
|
style: MyTextStyle.bodySmall(
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -148,9 +152,11 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
visualDensity: const VisualDensity(horizontal: -2, vertical: -2),
|
visualDensity:
|
||||||
|
const VisualDensity(horizontal: -2, vertical: -2),
|
||||||
padding: MaterialStateProperty.all(
|
padding: MaterialStateProperty.all(
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 6)),
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
),
|
||||||
backgroundColor: MaterialStateProperty.resolveWith(
|
backgroundColor: MaterialStateProperty.resolveWith(
|
||||||
(states) => states.contains(MaterialState.selected)
|
(states) => states.contains(MaterialState.selected)
|
||||||
? Colors.indigo.shade100
|
? Colors.indigo.shade100
|
||||||
@ -212,28 +218,32 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPaidByFilter(BuildContext context) {
|
||||||
/// Extracted widget builder for the "Paid By" employee filter.
|
|
||||||
Widget _buildPaidByFilter() {
|
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Paid By",
|
"Paid By",
|
||||||
_employeeSelector(
|
_employeeSelector(
|
||||||
selectedEmployees: expenseController.selectedPaidByEmployees),
|
context: context,
|
||||||
|
selectedEmployees: expenseController.selectedPaidByEmployees,
|
||||||
|
searchEmployees: searchEmployeesForBottomSheet, // FIXED
|
||||||
|
title: 'Search Paid By',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracted widget builder for the "Created By" employee filter.
|
Widget _buildCreatedByFilter(BuildContext context) {
|
||||||
Widget _buildCreatedByFilter() {
|
|
||||||
return _buildField(
|
return _buildField(
|
||||||
"Created By",
|
"Created By",
|
||||||
_employeeSelector(
|
_employeeSelector(
|
||||||
selectedEmployees: expenseController.selectedCreatedByEmployees),
|
context: context,
|
||||||
|
selectedEmployees: expenseController.selectedCreatedByEmployees,
|
||||||
|
searchEmployees: searchEmployeesForBottomSheet, // FIXED
|
||||||
|
title: 'Search Created By',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to show a date picker and update the state.
|
|
||||||
Future<void> _selectDate(
|
Future<void> _selectDate(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Rx<DateTime?> dateNotifier, {
|
Rx<DateTime?> dateNotifier, {
|
||||||
@ -251,7 +261,6 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reusable popup selector widget.
|
|
||||||
Widget _popupSelector(
|
Widget _popupSelector(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String currentValue,
|
required String currentValue,
|
||||||
@ -264,7 +273,7 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
itemBuilder: (context) => items
|
itemBuilder: (context) => items
|
||||||
.map((e) => PopupMenuItem<String>(
|
.map((e) => PopupMenuItem<String>(
|
||||||
value: e,
|
value: e,
|
||||||
child: Text(e),
|
child: MyText(e),
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -278,7 +287,7 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: MyText(
|
||||||
currentValue,
|
currentValue,
|
||||||
style: const TextStyle(color: Colors.black87),
|
style: const TextStyle(color: Colors.black87),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -291,7 +300,6 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reusable date button widget.
|
|
||||||
Widget _dateButton({required String label, required VoidCallback onTap}) {
|
Widget _dateButton({required String label, required VoidCallback onTap}) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@ -307,7 +315,7 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
const Icon(Icons.calendar_today, size: 16, color: Colors.grey),
|
const Icon(Icons.calendar_today, size: 16, color: Colors.grey),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: MyText(
|
||||||
label,
|
label,
|
||||||
style: MyTextStyle.bodyMedium(),
|
style: MyTextStyle.bodyMedium(),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -319,9 +327,36 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reusable employee selector with Autocomplete.
|
Future<void> _showEmployeeSelectorBottomSheet({
|
||||||
Widget _employeeSelector({required RxList<EmployeeModel> selectedEmployees}) {
|
required BuildContext context,
|
||||||
final textController = TextEditingController();
|
required RxList<EmployeeModel> selectedEmployees,
|
||||||
|
required Future<List<EmployeeModel>> Function(String) searchEmployees,
|
||||||
|
String title = 'Select Employee',
|
||||||
|
}) async {
|
||||||
|
final List<EmployeeModel>? result =
|
||||||
|
await showModalBottomSheet<List<EmployeeModel>>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (context) => EmployeeSelectorBottomSheet(
|
||||||
|
selectedEmployees: selectedEmployees,
|
||||||
|
searchEmployees: searchEmployees,
|
||||||
|
title: title,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result != null) {
|
||||||
|
selectedEmployees.assignAll(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _employeeSelector({
|
||||||
|
required BuildContext context,
|
||||||
|
required RxList<EmployeeModel> selectedEmployees,
|
||||||
|
required Future<List<EmployeeModel>> Function(String) searchEmployees,
|
||||||
|
String title = 'Search Employee',
|
||||||
|
}) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -331,102 +366,39 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 0,
|
|
||||||
children: selectedEmployees
|
children: selectedEmployees
|
||||||
.map((emp) => Chip(
|
.map((emp) => Chip(
|
||||||
label: Text(emp.name),
|
label: MyText(emp.name),
|
||||||
onDeleted: () => selectedEmployees.remove(emp),
|
onDeleted: () => selectedEmployees.remove(emp),
|
||||||
deleteIcon: const Icon(Icons.close, size: 18),
|
|
||||||
backgroundColor: Colors.grey.shade200,
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
Autocomplete<EmployeeModel>(
|
GestureDetector(
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
onTap: () => _showEmployeeSelectorBottomSheet(
|
||||||
if (textEditingValue.text.isEmpty) {
|
context: context,
|
||||||
return const Iterable<EmployeeModel>.empty();
|
selectedEmployees: selectedEmployees,
|
||||||
}
|
searchEmployees: searchEmployees,
|
||||||
return expenseController.allEmployees.where((emp) {
|
title: title,
|
||||||
final isNotSelected = !selectedEmployees.contains(emp);
|
),
|
||||||
final matchesQuery = emp.name
|
child: Container(
|
||||||
.toLowerCase()
|
padding: MySpacing.all(12),
|
||||||
.contains(textEditingValue.text.toLowerCase());
|
decoration: BoxDecoration(
|
||||||
return isNotSelected && matchesQuery;
|
color: Colors.grey.shade100,
|
||||||
});
|
borderRadius: BorderRadius.circular(12),
|
||||||
},
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
displayStringForOption: (EmployeeModel emp) => emp.name,
|
),
|
||||||
onSelected: (EmployeeModel emp) {
|
child: Row(
|
||||||
if (!selectedEmployees.contains(emp)) {
|
children: [
|
||||||
selectedEmployees.add(emp);
|
const Icon(Icons.search, color: Colors.grey),
|
||||||
}
|
MySpacing.width(8),
|
||||||
textController.clear();
|
Expanded(child: MyText(title)),
|
||||||
},
|
],
|
||||||
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
|
|
||||||
// Assign the local controller to the one from the builder
|
|
||||||
// to allow clearing it on selection.
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (textController != controller) {
|
|
||||||
// This is a workaround to sync controllers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
decoration: _inputDecoration("Search Employee"),
|
|
||||||
onSubmitted: (_) => onFieldSubmitted(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
optionsViewBuilder: (context, onSelected, options) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.white,
|
|
||||||
elevation: 4.0,
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 200),
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
itemCount: options.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final emp = options.elementAt(index);
|
|
||||||
return ListTile(
|
|
||||||
title: Text(emp.name),
|
|
||||||
onTap: () => onSelected(emp),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Centralized decoration for text fields.
|
|
||||||
InputDecoration _inputDecoration(String hint) {
|
|
||||||
return InputDecoration(
|
|
||||||
hintText: hint,
|
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.grey.shade100,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
|
||||||
),
|
|
||||||
contentPadding: MySpacing.all(12),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user