Vaibhav_Feature-#768 #59
@ -1,28 +1,86 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.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/model/expense/expense_list_model.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 {
|
class ExpenseController extends GetxController {
|
||||||
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
|
final RxList<ExpenseModel> expenses = <ExpenseModel>[].obs;
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxString errorMessage = ''.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
|
int _pageSize = 20;
|
||||||
Future<void> fetchExpenses() async {
|
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;
|
isLoading.value = true;
|
||||||
errorMessage.value = '';
|
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 {
|
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) {
|
if (result != null) {
|
||||||
try {
|
try {
|
||||||
// Convert the raw result (List<dynamic>) to List<ExpenseModel>
|
final List<dynamic> rawList = result['expenses'] ?? [];
|
||||||
final List<ExpenseModel> parsed = List<ExpenseModel>.from(
|
final parsed = rawList
|
||||||
result.map((e) => ExpenseModel.fromJson(e)));
|
.map((e) => ExpenseModel.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
expenses.assignAll(parsed);
|
expenses.assignAll(parsed);
|
||||||
logSafe("Expenses loaded: ${parsed.length}");
|
logSafe("Expenses loaded: ${parsed.length}");
|
||||||
|
logSafe(
|
||||||
|
"Pagination Info: Page ${result['currentPage']} of ${result['totalPages']} | Total: ${result['totalEntites']}");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage.value = 'Failed to parse expenses: $e';
|
errorMessage.value = 'Failed to parse expenses: $e';
|
||||||
logSafe("Parse error in fetchExpenses: $e", level: LogLevel.error);
|
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
|
/// Update expense status and refresh the list
|
||||||
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
|
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
@ -286,16 +286,52 @@ class ApiService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<dynamic>?> getExpenseListApi() async {
|
static Future<Map<String, dynamic>?> getExpenseListApi({
|
||||||
const endpoint = ApiEndpoints.getExpenseList;
|
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 {
|
try {
|
||||||
final response = await _getRequest(endpoint);
|
final response = await _getRequest(uri.toString());
|
||||||
if (response == null) return null;
|
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) {
|
} catch (e, stack) {
|
||||||
logSafe("Exception during getExpenseListApi: $e", level: LogLevel.error);
|
logSafe("Exception during getExpenseListApi: $e", level: LogLevel.error);
|
||||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
class DateTimeUtils {
|
class DateTimeUtils {
|
||||||
|
/// Converts a UTC datetime string to local time and formats it.
|
||||||
static String convertUtcToLocal(String utcTimeString, {String format = 'dd-MM-yyyy'}) {
|
static String convertUtcToLocal(String utcTimeString, {String format = 'dd-MM-yyyy'}) {
|
||||||
try {
|
try {
|
||||||
logSafe('convertUtcToLocal: input="$utcTimeString", format="$format"');
|
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'}) {
|
static String _formatDateTime(DateTime dateTime, {String format = 'dd-MM-yyyy'}) {
|
||||||
return DateFormat(format).format(dateTime);
|
return DateFormat(format).format(dateTime);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
List<ExpenseModel> expenseModelFromJson(String str) => List<ExpenseModel>.from(
|
List<ExpenseModel> expenseModelFromJson(String str) {
|
||||||
json.decode(str).map((x) => ExpenseModel.fromJson(x)));
|
final jsonData = json.decode(str);
|
||||||
|
return List<ExpenseModel>.from(
|
||||||
|
jsonData["data"]["data"].map((x) => ExpenseModel.fromJson(x))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String expenseModelToJson(List<ExpenseModel> data) =>
|
String expenseModelToJson(List<ExpenseModel> data) =>
|
||||||
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
|
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
|
||||||
@ -242,27 +246,35 @@ class CreatedBy {
|
|||||||
class Status {
|
class Status {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
|
final String displayName;
|
||||||
final String description;
|
final String description;
|
||||||
|
final String color;
|
||||||
final bool isSystem;
|
final bool isSystem;
|
||||||
|
|
||||||
Status({
|
Status({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.displayName,
|
||||||
required this.description,
|
required this.description,
|
||||||
|
required this.color,
|
||||||
required this.isSystem,
|
required this.isSystem,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Status.fromJson(Map<String, dynamic> json) => Status(
|
factory Status.fromJson(Map<String, dynamic> json) => Status(
|
||||||
id: json["id"],
|
id: json["id"],
|
||||||
name: json["name"],
|
name: json["name"],
|
||||||
|
displayName: json["displayName"],
|
||||||
description: json["description"],
|
description: json["description"],
|
||||||
|
color: json["color"],
|
||||||
isSystem: json["isSystem"],
|
isSystem: json["isSystem"],
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id,
|
"id": id,
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"displayName": displayName,
|
||||||
"description": description,
|
"description": description,
|
||||||
|
"color": color,
|
||||||
"isSystem": isSystem,
|
"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/model/expense/expense_list_model.dart';
|
||||||
import 'package:marco/view/expense/expense_detail_screen.dart';
|
import 'package:marco/view/expense/expense_detail_screen.dart';
|
||||||
import 'package:marco/model/expense/add_expense_bottom_sheet.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 {
|
class ExpenseMainScreen extends StatefulWidget {
|
||||||
const ExpenseMainScreen({super.key});
|
const ExpenseMainScreen({super.key});
|
||||||
@ -22,6 +24,9 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
final RxString searchQuery = ''.obs;
|
final RxString searchQuery = ''.obs;
|
||||||
final ProjectController projectController = Get.find<ProjectController>();
|
final ProjectController projectController = Get.find<ProjectController>();
|
||||||
final ExpenseController expenseController = Get.put(ExpenseController());
|
final ExpenseController expenseController = Get.put(ExpenseController());
|
||||||
|
final RxList<EmployeeModel> selectedPaidByEmployees = <EmployeeModel>[].obs;
|
||||||
|
final RxList<EmployeeModel> selectedCreatedByEmployees =
|
||||||
|
<EmployeeModel>[].obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -111,36 +116,10 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
|
|
||||||
void _openFilterBottomSheet() {
|
void _openFilterBottomSheet() {
|
||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
Container(
|
ExpenseFilterBottomSheet(
|
||||||
padding: const EdgeInsets.all(16),
|
expenseController: expenseController,
|
||||||
decoration: const BoxDecoration(
|
selectedPaidByEmployees: selectedPaidByEmployees,
|
||||||
color: Colors.white,
|
selectedCreatedByEmployees: selectedCreatedByEmployees,
|
||||||
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: () {},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -282,7 +261,7 @@ class _SearchAndFilter extends StatelessWidget {
|
|||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.tune, color: Colors.black),
|
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