feat(expense): implement expense filtering functionality with UI integration
This commit is contained in:
parent
e0ed35a671
commit
586d18565f
@ -119,7 +119,7 @@ class AddExpenseController extends GetxController {
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Failed to fetch master data: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Fetch Current Location ===
|
||||
Future<void> fetchCurrentLocation() async {
|
||||
|
@ -1,28 +1,86 @@
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/model/expense/expense_list_model.dart';
|
||||
import 'package:marco/model/expense/payment_types_model.dart';
|
||||
import 'package:marco/model/expense/expense_type_model.dart';
|
||||
import 'package:marco/model/expense/expense_status_model.dart';
|
||||
import 'package:marco/model/employee_model.dart';
|
||||
|
||||
class ExpenseController extends GetxController {
|
||||
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
||||
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
|
||||
final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
|
||||
final RxList<String> globalProjects = <String>[].obs;
|
||||
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
||||
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||
|
||||
/// Fetch all expenses from API
|
||||
Future<void> fetchExpenses() async {
|
||||
int _pageSize = 20;
|
||||
int _pageNumber = 1;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadInitialMasterData();
|
||||
fetchAllEmployees();
|
||||
}
|
||||
|
||||
/// Load projects, expense types, statuses, and payment modes on controller init
|
||||
Future<void> loadInitialMasterData() async {
|
||||
await fetchGlobalProjects();
|
||||
await fetchMasterData();
|
||||
}
|
||||
|
||||
/// Fetch expenses with filters and pagination (called explicitly when needed)
|
||||
Future<void> fetchExpenses({
|
||||
List<String>? projectIds,
|
||||
List<String>? statusIds,
|
||||
List<String>? createdByIds,
|
||||
List<String>? paidByIds,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
int pageSize = 20,
|
||||
int pageNumber = 1,
|
||||
}) async {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
_pageSize = pageSize;
|
||||
_pageNumber = pageNumber;
|
||||
|
||||
final Map<String, dynamic> filterMap = {
|
||||
"projectIds": projectIds ?? [],
|
||||
"statusIds": statusIds ?? [],
|
||||
"createdByIds": createdByIds ?? [],
|
||||
"paidByIds": paidByIds ?? [],
|
||||
"startDate": startDate?.toIso8601String(),
|
||||
"endDate": endDate?.toIso8601String(),
|
||||
};
|
||||
|
||||
try {
|
||||
final result = await ApiService.getExpenseListApi();
|
||||
logSafe("Fetching expenses with filter: ${jsonEncode(filterMap)}");
|
||||
|
||||
final result = await ApiService.getExpenseListApi(
|
||||
filter: jsonEncode(filterMap),
|
||||
pageSize: _pageSize,
|
||||
pageNumber: _pageNumber,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
try {
|
||||
// Convert the raw result (List<dynamic>) to List<ExpenseModel>
|
||||
final List<ExpenseModel> parsed = List<ExpenseModel>.from(
|
||||
result.map((e) => ExpenseModel.fromJson(e)));
|
||||
final List<dynamic> rawList = result['expenses'] ?? [];
|
||||
final parsed = rawList
|
||||
.map((e) => ExpenseModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
expenses.assignAll(parsed);
|
||||
logSafe("Expenses loaded: ${parsed.length}");
|
||||
logSafe(
|
||||
"Pagination Info: Page ${result['currentPage']} of ${result['totalPages']} | Total: ${result['totalEntites']}");
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Failed to parse expenses: $e';
|
||||
logSafe("Parse error in fetchExpenses: $e", level: LogLevel.error);
|
||||
@ -40,6 +98,82 @@ class ExpenseController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch master data: expense types, payment modes, and expense status
|
||||
Future<void> fetchMasterData() async {
|
||||
try {
|
||||
final expenseTypesData = await ApiService.getMasterExpenseTypes();
|
||||
if (expenseTypesData is List) {
|
||||
expenseTypes.value =
|
||||
expenseTypesData.map((e) => ExpenseTypeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
final paymentModesData = await ApiService.getMasterPaymentModes();
|
||||
if (paymentModesData is List) {
|
||||
paymentModes.value =
|
||||
paymentModesData.map((e) => PaymentModeModel.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
final expenseStatusData = await ApiService.getMasterExpenseStatus();
|
||||
if (expenseStatusData is List) {
|
||||
expenseStatuses.value = expenseStatusData
|
||||
.map((e) => ExpenseStatusModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar("Error", "Failed to fetch master data: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch list of all global projects
|
||||
Future<void> fetchGlobalProjects() async {
|
||||
try {
|
||||
final response = await ApiService.getGlobalProjects();
|
||||
if (response != null) {
|
||||
final names = <String>[];
|
||||
for (var item in response) {
|
||||
final name = item['name']?.toString().trim();
|
||||
final id = item['id']?.toString().trim();
|
||||
if (name != null && id != null && name.isNotEmpty) {
|
||||
projectsMap[name] = id;
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
globalProjects.assignAll(names);
|
||||
logSafe("Fetched ${names.length} global projects");
|
||||
}
|
||||
} catch (e) {
|
||||
logSafe("Failed to fetch global projects: $e", level: LogLevel.error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch all employees for Manage Bucket usage
|
||||
Future<void> fetchAllEmployees() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
final response = await ApiService.getAllEmployees();
|
||||
if (response != null && response.isNotEmpty) {
|
||||
allEmployees
|
||||
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
|
||||
logSafe(
|
||||
"All Employees fetched for Manage Bucket: ${allEmployees.length}",
|
||||
level: LogLevel.info,
|
||||
);
|
||||
} else {
|
||||
allEmployees.clear();
|
||||
logSafe("No employees found for Manage Bucket.",
|
||||
level: LogLevel.warning);
|
||||
}
|
||||
} catch (e) {
|
||||
allEmployees.clear();
|
||||
logSafe("Error fetching employees in Manage Bucket",
|
||||
level: LogLevel.error, error: e);
|
||||
}
|
||||
|
||||
isLoading.value = false;
|
||||
update();
|
||||
}
|
||||
|
||||
/// Update expense status and refresh the list
|
||||
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
|
||||
isLoading.value = true;
|
||||
@ -54,7 +188,7 @@ class ExpenseController extends GetxController {
|
||||
|
||||
if (success) {
|
||||
logSafe("Expense status updated successfully.");
|
||||
await fetchExpenses();
|
||||
await fetchExpenses();
|
||||
return true;
|
||||
} else {
|
||||
errorMessage.value = "Failed to update expense status.";
|
||||
|
@ -286,16 +286,52 @@ class ApiService {
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<List<dynamic>?> getExpenseListApi() async {
|
||||
const endpoint = ApiEndpoints.getExpenseList;
|
||||
static Future<Map<String, dynamic>?> getExpenseListApi({
|
||||
String? filter,
|
||||
int pageSize = 20,
|
||||
int pageNumber = 1,
|
||||
}) async {
|
||||
// Build the endpoint with query parameters
|
||||
String endpoint = ApiEndpoints.getExpenseList;
|
||||
final queryParams = <String, String>{
|
||||
'pageSize': pageSize.toString(),
|
||||
'pageNumber': pageNumber.toString(),
|
||||
};
|
||||
|
||||
logSafe("Fetching expense list...");
|
||||
if (filter != null && filter.isNotEmpty) {
|
||||
queryParams['filter'] = filter;
|
||||
}
|
||||
|
||||
// Build the full URI
|
||||
final uri = Uri.parse(endpoint).replace(queryParameters: queryParams);
|
||||
logSafe("Fetching expense list with URI: $uri");
|
||||
|
||||
try {
|
||||
final response = await _getRequest(endpoint);
|
||||
final response = await _getRequest(uri.toString());
|
||||
if (response == null) return null;
|
||||
|
||||
return _parseResponse(response, label: 'Expense List');
|
||||
final parsed = _parseResponseForAllData(response, label: 'Expense List');
|
||||
|
||||
if (parsed != null && parsed['data'] is Map<String, dynamic>) {
|
||||
final dataObj = parsed['data'] as Map<String, dynamic>;
|
||||
|
||||
if (dataObj['data'] is List) {
|
||||
return {
|
||||
'currentPage': dataObj['currentPage'] ?? 1,
|
||||
'totalPages': dataObj['totalPages'] ?? 1,
|
||||
'totalEntites': dataObj['totalEntites'] ?? 0,
|
||||
'expenses': List<dynamic>.from(dataObj['data']),
|
||||
};
|
||||
} else {
|
||||
logSafe("Expense list 'data' is not a list: $dataObj",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
logSafe("Unexpected response structure: $parsed",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception during getExpenseListApi: $e", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/app_logger.dart';
|
||||
|
||||
class DateTimeUtils {
|
||||
/// Converts a UTC datetime string to local time and formats it.
|
||||
static String convertUtcToLocal(String utcTimeString, {String format = 'dd-MM-yyyy'}) {
|
||||
try {
|
||||
logSafe('convertUtcToLocal: input="$utcTimeString", format="$format"');
|
||||
@ -32,6 +33,17 @@ class DateTimeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/// Public utility for formatting any DateTime.
|
||||
static String formatDate(DateTime date, String format) {
|
||||
try {
|
||||
return DateFormat(format).format(date);
|
||||
} catch (e, stackTrace) {
|
||||
logSafe('formatDate failed: $e', error: e, stackTrace: stackTrace);
|
||||
return 'Invalid Date';
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal formatter with default format.
|
||||
static String _formatDateTime(DateTime dateTime, {String format = 'dd-MM-yyyy'}) {
|
||||
return DateFormat(format).format(dateTime);
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
List<ExpenseModel> expenseModelFromJson(String str) => List<ExpenseModel>.from(
|
||||
json.decode(str).map((x) => ExpenseModel.fromJson(x)));
|
||||
List<ExpenseModel> expenseModelFromJson(String str) {
|
||||
final jsonData = json.decode(str);
|
||||
return List<ExpenseModel>.from(
|
||||
jsonData["data"]["data"].map((x) => ExpenseModel.fromJson(x))
|
||||
);
|
||||
}
|
||||
|
||||
String expenseModelToJson(List<ExpenseModel> data) =>
|
||||
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
|
||||
@ -242,27 +246,35 @@ class CreatedBy {
|
||||
class Status {
|
||||
final String id;
|
||||
final String name;
|
||||
final String displayName;
|
||||
final String description;
|
||||
final String color;
|
||||
final bool isSystem;
|
||||
|
||||
Status({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.displayName,
|
||||
required this.description,
|
||||
required this.color,
|
||||
required this.isSystem,
|
||||
});
|
||||
|
||||
factory Status.fromJson(Map<String, dynamic> json) => Status(
|
||||
id: json["id"],
|
||||
name: json["name"],
|
||||
displayName: json["displayName"],
|
||||
description: json["description"],
|
||||
color: json["color"],
|
||||
isSystem: json["isSystem"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"name": name,
|
||||
"displayName": displayName,
|
||||
"description": description,
|
||||
"color": color,
|
||||
"isSystem": isSystem,
|
||||
};
|
||||
}
|
||||
|
304
lib/view/expense/expense_filter_bottom_sheet.dart
Normal file
304
lib/view/expense/expense_filter_bottom_sheet.dart
Normal file
@ -0,0 +1,304 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
||||
import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/model/employee_model.dart';
|
||||
|
||||
class ExpenseFilterBottomSheet extends StatelessWidget {
|
||||
final ExpenseController expenseController;
|
||||
final RxList<EmployeeModel> selectedPaidByEmployees;
|
||||
final RxList<EmployeeModel> selectedCreatedByEmployees;
|
||||
|
||||
ExpenseFilterBottomSheet({
|
||||
super.key,
|
||||
required this.expenseController,
|
||||
required this.selectedPaidByEmployees,
|
||||
required this.selectedCreatedByEmployees,
|
||||
});
|
||||
|
||||
final RxString selectedProject = ''.obs;
|
||||
final Rx<DateTime?> startDate = Rx<DateTime?>(null);
|
||||
final Rx<DateTime?> endDate = Rx<DateTime?>(null);
|
||||
final RxString selectedEmployee = ''.obs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge('Filter Expenses', fontWeight: 700),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
/// Project Filter
|
||||
MyText.bodyMedium('Project', fontWeight: 600),
|
||||
const SizedBox(height: 6),
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedProject.value.isEmpty
|
||||
? null
|
||||
: selectedProject.value,
|
||||
items: expenseController.globalProjects
|
||||
.map((proj) => DropdownMenuItem(
|
||||
value: proj,
|
||||
child: Text(proj),
|
||||
))
|
||||
.toList(),
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
hint: const Text('Select Project'),
|
||||
onChanged: (value) {
|
||||
selectedProject.value = value ?? '';
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
/// Date Range Filter
|
||||
MyText.bodyMedium('Date Range', fontWeight: 600),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _DatePickerField(
|
||||
label: startDate.value == null
|
||||
? 'Start Date'
|
||||
: DateTimeUtils.formatDate(
|
||||
startDate.value!, 'dd MMM yyyy'),
|
||||
onTap: () async {
|
||||
DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: startDate.value ?? DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate:
|
||||
DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) startDate.value = picked;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _DatePickerField(
|
||||
label: endDate.value == null
|
||||
? 'End Date'
|
||||
: DateTimeUtils.formatDate(
|
||||
endDate.value!, 'dd MMM yyyy'),
|
||||
onTap: () async {
|
||||
DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: endDate.value ?? DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate:
|
||||
DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) endDate.value = picked;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
/// Paid By Filter
|
||||
_employeeFilterSection(
|
||||
title: 'Paid By',
|
||||
selectedEmployees: selectedPaidByEmployees,
|
||||
expenseController: expenseController,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
/// Created By Filter
|
||||
_employeeFilterSection(
|
||||
title: 'Created By',
|
||||
selectedEmployees: selectedCreatedByEmployees,
|
||||
expenseController: expenseController,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
foregroundColor: Colors.black,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
expenseController.fetchExpenses(
|
||||
projectIds: selectedProject.value.isEmpty
|
||||
? null
|
||||
: [
|
||||
expenseController
|
||||
.projectsMap[selectedProject.value]!
|
||||
],
|
||||
paidByIds: selectedPaidByEmployees.isEmpty
|
||||
? null
|
||||
: selectedPaidByEmployees.map((e) => e.id).toList(),
|
||||
createdByIds: selectedCreatedByEmployees.isEmpty
|
||||
? null
|
||||
: selectedCreatedByEmployees
|
||||
.map((e) => e.id)
|
||||
.toList(),
|
||||
startDate: startDate.value,
|
||||
endDate: endDate.value,
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('Apply'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Employee Filter Section
|
||||
Widget _employeeFilterSection({
|
||||
required String title,
|
||||
required RxList<EmployeeModel> selectedEmployees,
|
||||
required ExpenseController expenseController,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(title, fontWeight: 600),
|
||||
const SizedBox(height: 6),
|
||||
Obx(() {
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: -8,
|
||||
children: selectedEmployees.map((emp) {
|
||||
return Chip(
|
||||
label: Text(emp.name),
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
onDeleted: () => selectedEmployees.remove(emp),
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 6),
|
||||
Autocomplete<EmployeeModel>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return const Iterable<EmployeeModel>.empty();
|
||||
}
|
||||
return expenseController.allEmployees.where((EmployeeModel emp) {
|
||||
return emp.name
|
||||
.toLowerCase()
|
||||
.contains(textEditingValue.text.toLowerCase());
|
||||
});
|
||||
},
|
||||
displayStringForOption: (EmployeeModel emp) => emp.name,
|
||||
onSelected: (EmployeeModel emp) {
|
||||
if (!selectedEmployees.contains(emp)) {
|
||||
selectedEmployees.add(emp);
|
||||
}
|
||||
},
|
||||
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search Employee',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
);
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
elevation: 4.0,
|
||||
child: SizedBox(
|
||||
height: 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Date Picker Field
|
||||
class _DatePickerField extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _DatePickerField({
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(color: Colors.black87)),
|
||||
const Icon(Icons.calendar_today, size: 16, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/model/expense/expense_list_model.dart';
|
||||
import 'package:marco/view/expense/expense_detail_screen.dart';
|
||||
import 'package:marco/model/expense/add_expense_bottom_sheet.dart';
|
||||
import 'package:marco/model/employee_model.dart';
|
||||
import 'package:marco/view/expense/expense_filter_bottom_sheet.dart';
|
||||
|
||||
class ExpenseMainScreen extends StatefulWidget {
|
||||
const ExpenseMainScreen({super.key});
|
||||
@ -22,6 +24,9 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
||||
final RxString searchQuery = ''.obs;
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
final ExpenseController expenseController = Get.put(ExpenseController());
|
||||
final RxList<EmployeeModel> selectedPaidByEmployees = <EmployeeModel>[].obs;
|
||||
final RxList<EmployeeModel> selectedCreatedByEmployees =
|
||||
<EmployeeModel>[].obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -45,7 +50,7 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
||||
searchController: searchController,
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
onFilterTap: _openFilterBottomSheet,
|
||||
onRefreshTap: _refreshExpenses,
|
||||
onRefreshTap: _refreshExpenses,
|
||||
),
|
||||
_ToggleButtons(isHistoryView: isHistoryView),
|
||||
Expanded(
|
||||
@ -111,36 +116,10 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
||||
|
||||
void _openFilterBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Wrap(
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
MyText.bodyLarge(
|
||||
'Filter Expenses',
|
||||
fontWeight: 700,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.date_range),
|
||||
title: MyText.bodyMedium('Date Range'),
|
||||
onTap: () {},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.work_outline),
|
||||
title: MyText.bodyMedium('Project'),
|
||||
onTap: () {},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.check_circle_outline),
|
||||
title: MyText.bodyMedium('Status'),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
ExpenseFilterBottomSheet(
|
||||
expenseController: expenseController,
|
||||
selectedPaidByEmployees: selectedPaidByEmployees,
|
||||
selectedCreatedByEmployees: selectedCreatedByEmployees,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -282,7 +261,7 @@ class _SearchAndFilter extends StatelessWidget {
|
||||
MySpacing.width(8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune, color: Colors.black),
|
||||
onPressed: null, // Disabled as per request
|
||||
onPressed: onFilterTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user