Vaibhav_Feature-#768 #59
70
lib/controller/expense/expense_detail_controller.dart
Normal file
70
lib/controller/expense/expense_detail_controller.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
class ExpenseDetailController extends GetxController {
|
||||||
|
final Rx<ExpenseModel?> expense = Rx<ExpenseModel?>(null);
|
||||||
|
final RxBool isLoading = false.obs;
|
||||||
|
final RxString errorMessage = ''.obs;
|
||||||
|
|
||||||
|
/// Fetch expense details by ID
|
||||||
|
Future<void> fetchExpenseDetails(String expenseId) async {
|
||||||
|
isLoading.value = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
logSafe("Fetching expense details for ID: $expenseId");
|
||||||
|
|
||||||
|
final result = await ApiService.getExpenseDetailsApi(expenseId: expenseId);
|
||||||
|
if (result != null) {
|
||||||
|
try {
|
||||||
|
expense.value = ExpenseModel.fromJson(result);
|
||||||
|
logSafe("Expense details loaded successfully: ${expense.value?.id}");
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = 'Failed to parse expense details: $e';
|
||||||
|
logSafe("Parse error in fetchExpenseDetails: $e",
|
||||||
|
level: LogLevel.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage.value = 'Failed to fetch expense details from server.';
|
||||||
|
logSafe("fetchExpenseDetails failed: null response",
|
||||||
|
level: LogLevel.error);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
errorMessage.value = 'An unexpected error occurred.';
|
||||||
|
logSafe("Exception in fetchExpenseDetails: $e", level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update status for this specific expense
|
||||||
|
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
|
||||||
|
isLoading.value = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
try {
|
||||||
|
logSafe("Updating status for expense: $expenseId -> $statusId");
|
||||||
|
final success = await ApiService.updateExpenseStatusApi(
|
||||||
|
expenseId: expenseId,
|
||||||
|
statusId: statusId,
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
logSafe("Expense status updated successfully.");
|
||||||
|
await fetchExpenseDetails(expenseId); // Refresh details
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
errorMessage.value = "Failed to update expense status.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
errorMessage.value = 'An unexpected error occurred.';
|
||||||
|
logSafe("Exception in updateExpenseStatus: $e", level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,8 @@ 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;
|
||||||
|
|
||||||
|
// Master data
|
||||||
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
final RxList<ExpenseTypeModel> expenseTypes = <ExpenseTypeModel>[].obs;
|
||||||
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
|
final RxList<PaymentModeModel> paymentModes = <PaymentModeModel>[].obs;
|
||||||
final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
|
final RxList<ExpenseStatusModel> expenseStatuses = <ExpenseStatusModel>[].obs;
|
||||||
@ -19,6 +21,15 @@ class ExpenseController extends GetxController {
|
|||||||
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
final RxMap<String, String> projectsMap = <String, String>{}.obs;
|
||||||
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
|
// Persistent Filter States
|
||||||
|
final RxString selectedProject = ''.obs;
|
||||||
|
final RxString selectedStatus = ''.obs;
|
||||||
|
final Rx<DateTime?> startDate = Rx<DateTime?>(null);
|
||||||
|
final Rx<DateTime?> endDate = Rx<DateTime?>(null);
|
||||||
|
final RxList<EmployeeModel> selectedPaidByEmployees = <EmployeeModel>[].obs;
|
||||||
|
final RxList<EmployeeModel> selectedCreatedByEmployees =
|
||||||
|
<EmployeeModel>[].obs;
|
||||||
|
|
||||||
int _pageSize = 20;
|
int _pageSize = 20;
|
||||||
int _pageNumber = 1;
|
int _pageNumber = 1;
|
||||||
|
|
||||||
@ -29,13 +40,22 @@ class ExpenseController extends GetxController {
|
|||||||
fetchAllEmployees();
|
fetchAllEmployees();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load projects, expense types, statuses, and payment modes on controller init
|
bool get isFilterApplied {
|
||||||
|
return selectedProject.value.isNotEmpty ||
|
||||||
|
selectedStatus.value.isNotEmpty ||
|
||||||
|
startDate.value != null ||
|
||||||
|
endDate.value != null ||
|
||||||
|
selectedPaidByEmployees.isNotEmpty ||
|
||||||
|
selectedCreatedByEmployees.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load master data
|
||||||
Future<void> loadInitialMasterData() async {
|
Future<void> loadInitialMasterData() async {
|
||||||
await fetchGlobalProjects();
|
await fetchGlobalProjects();
|
||||||
await fetchMasterData();
|
await fetchMasterData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch expenses with filters and pagination (called explicitly when needed)
|
/// Fetch expenses using filters
|
||||||
Future<void> fetchExpenses({
|
Future<void> fetchExpenses({
|
||||||
List<String>? projectIds,
|
List<String>? projectIds,
|
||||||
List<String>? statusIds,
|
List<String>? statusIds,
|
||||||
@ -53,12 +73,18 @@ class ExpenseController extends GetxController {
|
|||||||
_pageNumber = pageNumber;
|
_pageNumber = pageNumber;
|
||||||
|
|
||||||
final Map<String, dynamic> filterMap = {
|
final Map<String, dynamic> filterMap = {
|
||||||
"projectIds": projectIds ?? [],
|
"projectIds": projectIds ??
|
||||||
"statusIds": statusIds ?? [],
|
(selectedProject.value.isEmpty
|
||||||
"createdByIds": createdByIds ?? [],
|
? []
|
||||||
"paidByIds": paidByIds ?? [],
|
: [projectsMap[selectedProject.value] ?? '']),
|
||||||
"startDate": startDate?.toIso8601String(),
|
"statusIds": statusIds ??
|
||||||
"endDate": endDate?.toIso8601String(),
|
(selectedStatus.value.isEmpty ? [] : [selectedStatus.value]),
|
||||||
|
"createdByIds":
|
||||||
|
createdByIds ?? selectedCreatedByEmployees.map((e) => e.id).toList(),
|
||||||
|
"paidByIds":
|
||||||
|
paidByIds ?? selectedPaidByEmployees.map((e) => e.id).toList(),
|
||||||
|
"startDate": (startDate ?? this.startDate.value)?.toIso8601String(),
|
||||||
|
"endDate": (endDate ?? this.endDate.value)?.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -95,6 +121,16 @@ class ExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear all filters
|
||||||
|
void clearFilters() {
|
||||||
|
selectedProject.value = '';
|
||||||
|
selectedStatus.value = '';
|
||||||
|
startDate.value = null;
|
||||||
|
endDate.value = null;
|
||||||
|
selectedPaidByEmployees.clear();
|
||||||
|
selectedCreatedByEmployees.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch master data: expense types, payment modes, and expense status
|
/// Fetch master data: expense types, payment modes, and expense status
|
||||||
Future<void> fetchMasterData() async {
|
Future<void> fetchMasterData() async {
|
||||||
try {
|
try {
|
||||||
@ -121,7 +157,7 @@ class ExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch list of all global projects
|
/// Fetch global projects
|
||||||
Future<void> fetchGlobalProjects() async {
|
Future<void> fetchGlobalProjects() async {
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getGlobalProjects();
|
final response = await ApiService.getGlobalProjects();
|
||||||
@ -143,10 +179,9 @@ class ExpenseController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch all employees for Manage Bucket usage
|
/// Fetch all employees
|
||||||
Future<void> fetchAllEmployees() async {
|
Future<void> fetchAllEmployees() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await ApiService.getAllEmployees();
|
final response = await ApiService.getAllEmployees();
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
@ -166,23 +201,20 @@ class ExpenseController extends GetxController {
|
|||||||
logSafe("Error fetching employees in Manage Bucket",
|
logSafe("Error fetching employees in Manage Bucket",
|
||||||
level: LogLevel.error, error: e);
|
level: LogLevel.error, error: e);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update expense status and refresh the list
|
/// Update expense status
|
||||||
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
|
Future<bool> updateExpenseStatus(String expenseId, String statusId) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logSafe("Updating status for expense: $expenseId -> $statusId");
|
logSafe("Updating status for expense: $expenseId -> $statusId");
|
||||||
final success = await ApiService.updateExpenseStatusApi(
|
final success = await ApiService.updateExpenseStatusApi(
|
||||||
expenseId: expenseId,
|
expenseId: expenseId,
|
||||||
statusId: statusId,
|
statusId: statusId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
logSafe("Expense status updated successfully.");
|
logSafe("Expense status updated successfully.");
|
||||||
await fetchExpenses();
|
await fetchExpenses();
|
||||||
|
@ -241,6 +241,52 @@ class ApiService {
|
|||||||
|
|
||||||
// === Expense APIs === //
|
// === Expense APIs === //
|
||||||
|
|
||||||
|
/// Get Expense Details API
|
||||||
|
static Future<Map<String, dynamic>?> getExpenseDetailsApi({
|
||||||
|
required String expenseId,
|
||||||
|
}) async {
|
||||||
|
final endpoint = "${ApiEndpoints.getExpenseDetails}/$expenseId";
|
||||||
|
logSafe("Fetching expense details for ID: $expenseId");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _getRequest(endpoint);
|
||||||
|
if (response == null) {
|
||||||
|
logSafe("Expense details request failed: null response",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = response.body.trim();
|
||||||
|
if (body.isEmpty) {
|
||||||
|
logSafe("Expense details response body is empty",
|
||||||
|
level: LogLevel.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final jsonResponse = jsonDecode(body);
|
||||||
|
if (jsonResponse is Map<String, dynamic>) {
|
||||||
|
if (jsonResponse['success'] == true) {
|
||||||
|
logSafe("Expense details fetched successfully");
|
||||||
|
return jsonResponse['data']; // Return the expense details object
|
||||||
|
} else {
|
||||||
|
logSafe(
|
||||||
|
"Failed to fetch expense details: ${jsonResponse['message'] ?? 'Unknown error'}",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logSafe("Unexpected response structure: $jsonResponse",
|
||||||
|
level: LogLevel.error);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Exception during getExpenseDetailsApi: $e",
|
||||||
|
level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Update Expense Status API
|
/// Update Expense Status API
|
||||||
static Future<bool> updateExpenseStatusApi({
|
static Future<bool> updateExpenseStatusApi({
|
||||||
required String expenseId,
|
required String expenseId,
|
||||||
|
@ -4,36 +4,34 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
|||||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||||
|
|
||||||
class SkeletonLoaders {
|
class SkeletonLoaders {
|
||||||
|
static Widget buildLoadingSkeleton() {
|
||||||
static Widget buildLoadingSkeleton() {
|
return SizedBox(
|
||||||
return SizedBox(
|
height: 360,
|
||||||
height: 360,
|
child: Column(
|
||||||
child: Column(
|
children: List.generate(5, (index) {
|
||||||
children: List.generate(5, (index) {
|
return Padding(
|
||||||
return Padding(
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
child: SingleChildScrollView(
|
||||||
child: SingleChildScrollView(
|
scrollDirection: Axis.horizontal,
|
||||||
scrollDirection: Axis.horizontal,
|
child: Row(
|
||||||
child: Row(
|
children: List.generate(6, (i) {
|
||||||
children: List.generate(6, (i) {
|
return Container(
|
||||||
return Container(
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
width: 48,
|
||||||
width: 48,
|
height: 16,
|
||||||
height: 16,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Colors.grey.shade300,
|
||||||
color: Colors.grey.shade300,
|
borderRadius: BorderRadius.circular(6),
|
||||||
borderRadius: BorderRadius.circular(6),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Employee List - Card Style
|
// Employee List - Card Style
|
||||||
static Widget employeeListSkeletonLoader() {
|
static Widget employeeListSkeletonLoader() {
|
||||||
@ -63,25 +61,37 @@ static Widget buildLoadingSkeleton() {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(height: 14, width: 100, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 100,
|
||||||
|
color: Colors.grey.shade300),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Container(height: 12, width: 60, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 12, width: 60, color: Colors.grey.shade300),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.email, size: 16, color: Colors.grey.shade300),
|
Icon(Icons.email,
|
||||||
|
size: 16, color: Colors.grey.shade300),
|
||||||
MySpacing.width(4),
|
MySpacing.width(4),
|
||||||
Container(height: 10, width: 140, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 10,
|
||||||
|
width: 140,
|
||||||
|
color: Colors.grey.shade300),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.phone, size: 16, color: Colors.grey.shade300),
|
Icon(Icons.phone,
|
||||||
|
size: 16, color: Colors.grey.shade300),
|
||||||
MySpacing.width(4),
|
MySpacing.width(4),
|
||||||
Container(height: 10, width: 100, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 10,
|
||||||
|
width: 100,
|
||||||
|
color: Colors.grey.shade300),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -122,16 +132,28 @@ static Widget buildLoadingSkeleton() {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(height: 12, width: 100, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 100,
|
||||||
|
color: Colors.grey.shade300),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
Container(height: 10, width: 80, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 10,
|
||||||
|
width: 80,
|
||||||
|
color: Colors.grey.shade300),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Container(height: 28, width: 60, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 28,
|
||||||
|
width: 60,
|
||||||
|
color: Colors.grey.shade300),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Container(height: 28, width: 60, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 28,
|
||||||
|
width: 60,
|
||||||
|
color: Colors.grey.shade300),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -167,7 +189,8 @@ static Widget buildLoadingSkeleton() {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Container(height: 14, width: 120, color: Colors.grey.shade300),
|
Container(
|
||||||
|
height: 14, width: 120, color: Colors.grey.shade300),
|
||||||
Icon(Icons.add_circle, color: Colors.grey.shade300),
|
Icon(Icons.add_circle, color: Colors.grey.shade300),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -226,133 +249,198 @@ static Widget buildLoadingSkeleton() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
static Widget employeeSkeletonCard() {
|
|
||||||
return MyCard.bordered(
|
|
||||||
margin: MySpacing.only(bottom: 12),
|
|
||||||
paddingAll: 12,
|
|
||||||
borderRadiusAll: 12,
|
|
||||||
shadow: MyShadow(
|
|
||||||
elevation: 1.5,
|
|
||||||
position: MyShadowPosition.bottom,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Avatar
|
|
||||||
Container(
|
|
||||||
height: 35,
|
|
||||||
width: 35,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
|
|
||||||
// Name, org, email, phone
|
static Widget expenseListSkeletonLoader() {
|
||||||
Expanded(
|
return ListView.separated(
|
||||||
child: Column(
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
itemCount: 6, // Show 6 skeleton items
|
||||||
children: [
|
separatorBuilder: (_, __) =>
|
||||||
Container(height: 12, width: 120, color: Colors.grey.shade300),
|
Divider(color: Colors.grey.shade300, height: 20),
|
||||||
MySpacing.height(6),
|
itemBuilder: (context, index) {
|
||||||
Container(height: 10, width: 80, color: Colors.grey.shade300),
|
return Column(
|
||||||
MySpacing.height(8),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
||||||
// Email placeholder
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.email_outlined, size: 14, color: Colors.grey.shade300),
|
|
||||||
MySpacing.width(4),
|
|
||||||
Container(height: 10, width: 140, color: Colors.grey.shade300),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
|
|
||||||
// Phone placeholder
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.phone_outlined, size: 14, color: Colors.grey.shade300),
|
|
||||||
MySpacing.width(4),
|
|
||||||
Container(height: 10, width: 100, color: Colors.grey.shade300),
|
|
||||||
MySpacing.width(8),
|
|
||||||
Container(
|
|
||||||
height: 16,
|
|
||||||
width: 16,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
|
|
||||||
// Tags placeholder
|
|
||||||
Container(height: 8, width: 80, color: Colors.grey.shade300),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Arrow
|
|
||||||
Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey.shade300),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Widget contactSkeletonCard() {
|
|
||||||
return MyCard.bordered(
|
|
||||||
margin: MySpacing.only(bottom: 12),
|
|
||||||
paddingAll: 16,
|
|
||||||
borderRadiusAll: 16,
|
|
||||||
shadow: MyShadow(
|
|
||||||
elevation: 1.5,
|
|
||||||
position: MyShadowPosition.bottom,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Container(
|
// Title and Amount
|
||||||
height: 40,
|
Row(
|
||||||
width: 40,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
color: Colors.grey.shade300,
|
Container(
|
||||||
shape: BoxShape.circle,
|
height: 14,
|
||||||
),
|
width: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
MySpacing.width(12),
|
const SizedBox(height: 6),
|
||||||
Expanded(
|
// Date and Status
|
||||||
child: Column(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
height: 12,
|
||||||
height: 12,
|
width: 100,
|
||||||
width: 100,
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade300,
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
MySpacing.height(6),
|
),
|
||||||
Container(
|
const Spacer(),
|
||||||
height: 10,
|
Container(
|
||||||
width: 60,
|
height: 12,
|
||||||
|
width: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade300,
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
MySpacing.height(16),
|
},
|
||||||
Container(height: 10, width: 150, color: Colors.grey.shade300),
|
);
|
||||||
MySpacing.height(8),
|
}
|
||||||
Container(height: 10, width: 100, color: Colors.grey.shade300),
|
|
||||||
MySpacing.height(8),
|
|
||||||
Container(height: 10, width: 120, color: Colors.grey.shade300),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
static Widget employeeSkeletonCard() {
|
||||||
|
return MyCard.bordered(
|
||||||
|
margin: MySpacing.only(bottom: 12),
|
||||||
|
paddingAll: 12,
|
||||||
|
borderRadiusAll: 12,
|
||||||
|
shadow: MyShadow(
|
||||||
|
elevation: 1.5,
|
||||||
|
position: MyShadowPosition.bottom,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Avatar
|
||||||
|
Container(
|
||||||
|
height: 35,
|
||||||
|
width: 35,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(12),
|
||||||
|
|
||||||
|
// Name, org, email, phone
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(height: 12, width: 120, color: Colors.grey.shade300),
|
||||||
|
MySpacing.height(6),
|
||||||
|
Container(height: 10, width: 80, color: Colors.grey.shade300),
|
||||||
|
MySpacing.height(8),
|
||||||
|
|
||||||
|
// Email placeholder
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.email_outlined,
|
||||||
|
size: 14, color: Colors.grey.shade300),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Container(
|
||||||
|
height: 10, width: 140, color: Colors.grey.shade300),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(8),
|
||||||
|
|
||||||
|
// Phone placeholder
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.phone_outlined,
|
||||||
|
size: 14, color: Colors.grey.shade300),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Container(
|
||||||
|
height: 10, width: 100, color: Colors.grey.shade300),
|
||||||
|
MySpacing.width(8),
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(8),
|
||||||
|
|
||||||
|
// Tags placeholder
|
||||||
|
Container(height: 8, width: 80, color: Colors.grey.shade300),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Arrow
|
||||||
|
Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey.shade300),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget contactSkeletonCard() {
|
||||||
|
return MyCard.bordered(
|
||||||
|
margin: MySpacing.only(bottom: 12),
|
||||||
|
paddingAll: 16,
|
||||||
|
borderRadiusAll: 16,
|
||||||
|
shadow: MyShadow(
|
||||||
|
elevation: 1.5,
|
||||||
|
position: MyShadowPosition.bottom,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 100,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
MySpacing.height(6),
|
||||||
|
Container(
|
||||||
|
height: 10,
|
||||||
|
width: 60,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
Container(height: 10, width: 150, color: Colors.grey.shade300),
|
||||||
|
MySpacing.height(8),
|
||||||
|
Container(height: 10, width: 100, color: Colors.grey.shade300),
|
||||||
|
MySpacing.height(8),
|
||||||
|
Container(height: 10, width: 120, color: Colors.grey.shade300),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,15 +24,19 @@ class ExpenseResponse {
|
|||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ExpenseResponse.fromJson(Map<String, dynamic> json) =>
|
factory ExpenseResponse.fromJson(Map<String, dynamic> json) {
|
||||||
ExpenseResponse(
|
final dataField = json["data"];
|
||||||
success: json["success"],
|
return ExpenseResponse(
|
||||||
message: json["message"],
|
success: json["success"] ?? false,
|
||||||
data: ExpenseData.fromJson(json["data"]),
|
message: json["message"] ?? '',
|
||||||
errors: json["errors"],
|
data: (dataField is Map<String, dynamic>)
|
||||||
statusCode: json["statusCode"],
|
? ExpenseData.fromJson(dataField)
|
||||||
timestamp: DateTime.parse(json["timestamp"]),
|
: ExpenseData.empty(),
|
||||||
);
|
errors: json["errors"],
|
||||||
|
statusCode: json["statusCode"] ?? 0,
|
||||||
|
timestamp: DateTime.tryParse(json["timestamp"] ?? '') ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"success": success,
|
"success": success,
|
||||||
@ -45,12 +49,14 @@ class ExpenseResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ExpenseData {
|
class ExpenseData {
|
||||||
|
final Filter? filter;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
final int totalPages;
|
final int totalPages;
|
||||||
final int totalEntites;
|
final int totalEntites;
|
||||||
final List<ExpenseModel> data;
|
final List<ExpenseModel> data;
|
||||||
|
|
||||||
ExpenseData({
|
ExpenseData({
|
||||||
|
required this.filter,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
required this.totalPages,
|
required this.totalPages,
|
||||||
required this.totalEntites,
|
required this.totalEntites,
|
||||||
@ -58,14 +64,25 @@ class ExpenseData {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory ExpenseData.fromJson(Map<String, dynamic> json) => ExpenseData(
|
factory ExpenseData.fromJson(Map<String, dynamic> json) => ExpenseData(
|
||||||
currentPage: json["currentPage"],
|
filter: json["filter"] != null ? Filter.fromJson(json["filter"]) : null,
|
||||||
totalPages: json["totalPages"],
|
currentPage: json["currentPage"] ?? 0,
|
||||||
totalEntites: json["totalEntites"],
|
totalPages: json["totalPages"] ?? 0,
|
||||||
data: List<ExpenseModel>.from(
|
totalEntites: json["totalEntites"] ?? 0,
|
||||||
json["data"].map((x) => ExpenseModel.fromJson(x))),
|
data: (json["data"] as List<dynamic>? ?? [])
|
||||||
|
.map((x) => ExpenseModel.fromJson(x))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
factory ExpenseData.empty() => ExpenseData(
|
||||||
|
filter: null,
|
||||||
|
currentPage: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
totalEntites: 0,
|
||||||
|
data: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
"filter": filter?.toJson(),
|
||||||
"currentPage": currentPage,
|
"currentPage": currentPage,
|
||||||
"totalPages": totalPages,
|
"totalPages": totalPages,
|
||||||
"totalEntites": totalEntites,
|
"totalEntites": totalEntites,
|
||||||
@ -73,6 +90,47 @@ class ExpenseData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Filter {
|
||||||
|
final List<String> projectIds;
|
||||||
|
final List<String> statusIds;
|
||||||
|
final List<String> createdByIds;
|
||||||
|
final List<String> paidById;
|
||||||
|
final DateTime? startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
|
||||||
|
Filter({
|
||||||
|
required this.projectIds,
|
||||||
|
required this.statusIds,
|
||||||
|
required this.createdByIds,
|
||||||
|
required this.paidById,
|
||||||
|
required this.startDate,
|
||||||
|
required this.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Filter.fromJson(Map<String, dynamic> json) => Filter(
|
||||||
|
projectIds: List<String>.from(json["projectIds"] ?? []),
|
||||||
|
statusIds: List<String>.from(json["statusIds"] ?? []),
|
||||||
|
createdByIds: List<String>.from(json["createdByIds"] ?? []),
|
||||||
|
paidById: List<String>.from(json["paidById"] ?? []),
|
||||||
|
startDate:
|
||||||
|
json["startDate"] != null ? DateTime.tryParse(json["startDate"]) : null,
|
||||||
|
endDate:
|
||||||
|
json["endDate"] != null ? DateTime.tryParse(json["endDate"]) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"projectIds": projectIds,
|
||||||
|
"statusIds": statusIds,
|
||||||
|
"createdByIds": createdByIds,
|
||||||
|
"paidById": paidById,
|
||||||
|
"startDate": startDate?.toIso8601String(),
|
||||||
|
"endDate": endDate?.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ExpenseModel and other classes remain same as you wrote ---
|
||||||
|
// I will include them here for completeness.
|
||||||
|
|
||||||
class ExpenseModel {
|
class ExpenseModel {
|
||||||
final String id;
|
final String id;
|
||||||
final Project project;
|
final Project project;
|
||||||
@ -105,22 +163,22 @@ class ExpenseModel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory ExpenseModel.fromJson(Map<String, dynamic> json) => ExpenseModel(
|
factory ExpenseModel.fromJson(Map<String, dynamic> json) => ExpenseModel(
|
||||||
id: json["id"],
|
id: json["id"] ?? '',
|
||||||
project: Project.fromJson(json["project"]),
|
project: Project.fromJson(json["project"] ?? {}),
|
||||||
expensesType: ExpenseType.fromJson(json["expensesType"]),
|
expensesType: ExpenseType.fromJson(json["expensesType"] ?? {}),
|
||||||
paymentMode: PaymentMode.fromJson(json["paymentMode"]),
|
paymentMode: PaymentMode.fromJson(json["paymentMode"] ?? {}),
|
||||||
paidBy: PaidBy.fromJson(json["paidBy"]),
|
paidBy: PaidBy.fromJson(json["paidBy"] ?? {}),
|
||||||
createdBy: CreatedBy.fromJson(json["createdBy"]),
|
createdBy: CreatedBy.fromJson(json["createdBy"] ?? {}),
|
||||||
transactionDate: DateTime.parse(json["transactionDate"]),
|
transactionDate:
|
||||||
createdAt: DateTime.parse(json["createdAt"]),
|
DateTime.tryParse(json["transactionDate"] ?? '') ?? DateTime.now(),
|
||||||
supplerName: json["supplerName"],
|
createdAt:
|
||||||
amount: (json["amount"] as num).toDouble(),
|
DateTime.tryParse(json["createdAt"] ?? '') ?? DateTime.now(),
|
||||||
status: Status.fromJson(json["status"]),
|
supplerName: json["supplerName"] ?? '',
|
||||||
nextStatus: json["nextStatus"] != null
|
amount: (json["amount"] ?? 0).toDouble(),
|
||||||
? List<Status>.from(
|
status: Status.fromJson(json["status"] ?? {}),
|
||||||
json["nextStatus"].map((x) => Status.fromJson(x)),
|
nextStatus: (json["nextStatus"] as List<dynamic>? ?? [])
|
||||||
)
|
.map((x) => Status.fromJson(x))
|
||||||
: [],
|
.toList(),
|
||||||
preApproved: json["preApproved"] ?? false,
|
preApproved: json["preApproved"] ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -163,14 +221,15 @@ class Project {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory Project.fromJson(Map<String, dynamic> json) => Project(
|
factory Project.fromJson(Map<String, dynamic> json) => Project(
|
||||||
id: json["id"],
|
id: json["id"] ?? '',
|
||||||
name: json["name"],
|
name: json["name"] ?? '',
|
||||||
shortName: json["shortName"],
|
shortName: json["shortName"] ?? '',
|
||||||
projectAddress: json["projectAddress"],
|
projectAddress: json["projectAddress"] ?? '',
|
||||||
contactPerson: json["contactPerson"],
|
contactPerson: json["contactPerson"] ?? '',
|
||||||
startDate: DateTime.parse(json["startDate"]),
|
startDate:
|
||||||
endDate: DateTime.parse(json["endDate"]),
|
DateTime.tryParse(json["startDate"] ?? '') ?? DateTime.now(),
|
||||||
projectStatusId: json["projectStatusId"],
|
endDate: DateTime.tryParse(json["endDate"] ?? '') ?? DateTime.now(),
|
||||||
|
projectStatusId: json["projectStatusId"] ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@ -199,10 +258,10 @@ class ExpenseType {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory ExpenseType.fromJson(Map<String, dynamic> json) => ExpenseType(
|
factory ExpenseType.fromJson(Map<String, dynamic> json) => ExpenseType(
|
||||||
id: json["id"],
|
id: json["id"] ?? '',
|
||||||
name: json["name"],
|
name: json["name"] ?? '',
|
||||||
noOfPersonsRequired: json["noOfPersonsRequired"],
|
noOfPersonsRequired: json["noOfPersonsRequired"] ?? false,
|
||||||
description: json["description"],
|
description: json["description"] ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@ -225,9 +284,9 @@ class PaymentMode {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory PaymentMode.fromJson(Map<String, dynamic> json) => PaymentMode(
|
factory PaymentMode.fromJson(Map<String, dynamic> json) => PaymentMode(
|
||||||
id: json["id"],
|
id: json["id"] ?? '',
|
||||||
name: json["name"],
|
name: json["name"] ?? '',
|
||||||
description: json["description"],
|
description: json["description"] ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@ -255,11 +314,11 @@ class PaidBy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory PaidBy.fromJson(Map<String, dynamic> json) => PaidBy(
|
factory PaidBy.fromJson(Map<String, dynamic> json) => PaidBy(
|
||||||
id: json["id"],
|
id: json["id"] ?? '',
|
||||||
firstName: json["firstName"],
|
firstName: json["firstName"] ?? '',
|
||||||
lastName: json["lastName"],
|
lastName: json["lastName"] ?? '',
|
||||||
photo: json["photo"],
|
photo: json["photo"] ?? '',
|
||||||
jobRoleId: json["jobRoleId"],
|
jobRoleId: json["jobRoleId"] ?? '',
|
||||||
jobRoleName: json["jobRoleName"],
|
jobRoleName: json["jobRoleName"],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -291,11 +350,11 @@ class CreatedBy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory CreatedBy.fromJson(Map<String, dynamic> json) => CreatedBy(
|
factory CreatedBy.fromJson(Map<String, dynamic> json) => CreatedBy(
|
||||||
id: json["id"],
|
id: json["id"] ?? '',
|
||||||
firstName: json["firstName"],
|
firstName: json["firstName"] ?? '',
|
||||||
lastName: json["lastName"],
|
lastName: json["lastName"] ?? '',
|
||||||
photo: json["photo"],
|
photo: json["photo"] ?? '',
|
||||||
jobRoleId: json["jobRoleId"],
|
jobRoleId: json["jobRoleId"] ?? '',
|
||||||
jobRoleName: json["jobRoleName"],
|
jobRoleName: json["jobRoleName"],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -327,12 +386,12 @@ class Status {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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"],
|
displayName: json["displayName"] ?? '',
|
||||||
description: json["description"],
|
description: json["description"] ?? '',
|
||||||
color: json["color"],
|
color: (json["color"] ?? '').replaceAll("'", ''),
|
||||||
isSystem: json["isSystem"],
|
isSystem: json["isSystem"] ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
@ -1,27 +1,34 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:marco/controller/expense/expense_screen_controller.dart';
|
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
||||||
|
import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
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/model/expense/expense_list_model.dart';
|
import 'package:marco/model/expense/expense_list_model.dart';
|
||||||
import 'package:marco/helpers/utils/date_time_utils.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class ExpenseDetailScreen extends StatelessWidget {
|
class ExpenseDetailScreen extends StatelessWidget {
|
||||||
const ExpenseDetailScreen({super.key});
|
final String expenseId;
|
||||||
|
|
||||||
static Color getStatusColor(String? status) {
|
const ExpenseDetailScreen({super.key, required this.expenseId});
|
||||||
|
|
||||||
|
// Status color logic
|
||||||
|
static Color getStatusColor(String? status, {String? colorCode}) {
|
||||||
|
if (colorCode != null && colorCode.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
return Color(int.parse(colorCode.replaceFirst('#', '0xff')));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'Requested':
|
case 'Approval Pending':
|
||||||
return Colors.blue;
|
|
||||||
case 'Review':
|
|
||||||
return Colors.orange;
|
return Colors.orange;
|
||||||
case 'Approved':
|
case 'Process Pending':
|
||||||
return Colors.green;
|
return Colors.blue;
|
||||||
|
case 'Rejected':
|
||||||
|
return Colors.red;
|
||||||
case 'Paid':
|
case 'Paid':
|
||||||
return Colors.purple;
|
return Colors.green;
|
||||||
case 'Closed':
|
|
||||||
return Colors.grey;
|
|
||||||
default:
|
default:
|
||||||
return Colors.black;
|
return Colors.black;
|
||||||
}
|
}
|
||||||
@ -29,12 +36,9 @@ class ExpenseDetailScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ExpenseModel expense = Get.arguments['expense'] as ExpenseModel;
|
final controller = Get.put(ExpenseDetailController());
|
||||||
final statusColor = getStatusColor(expense.status.name);
|
|
||||||
final projectController = Get.find<ProjectController>();
|
final projectController = Get.find<ProjectController>();
|
||||||
final expenseController = Get.find<ExpenseController>();
|
controller.fetchExpenseDetails(expenseId);
|
||||||
print(
|
|
||||||
"Next Status List: ${expense.nextStatus.map((e) => e.toJson()).toList()}");
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF7F7F7),
|
backgroundColor: const Color(0xFFF7F7F7),
|
||||||
@ -48,12 +52,12 @@ class ExpenseDetailScreen extends StatelessWidget {
|
|||||||
title: Padding(
|
title: Padding(
|
||||||
padding: MySpacing.xy(16, 0),
|
padding: MySpacing.xy(16, 0),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
icon: const Icon(Icons.arrow_back_ios_new,
|
||||||
color: Colors.black, size: 20),
|
color: Colors.black, size: 20),
|
||||||
onPressed: () => Get.back(),
|
onPressed: () =>
|
||||||
|
Get.offAllNamed('/dashboard/expense-main-page'),
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -98,84 +102,146 @@ class ExpenseDetailScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SafeArea(
|
||||||
padding: const EdgeInsets.all(16),
|
child: Obx(() {
|
||||||
child: Column(
|
if (controller.isLoading.value) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
return _buildLoadingSkeleton();
|
||||||
children: [
|
}
|
||||||
_ExpenseHeader(
|
if (controller.errorMessage.isNotEmpty) {
|
||||||
title: expense.expensesType.name,
|
return Center(
|
||||||
amount: '₹ ${expense.amount.toStringAsFixed(2)}',
|
child: Text(
|
||||||
status: expense.status.name,
|
controller.errorMessage.value,
|
||||||
statusColor: statusColor,
|
style: const TextStyle(color: Colors.red, fontSize: 16),
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_ExpenseDetailsList(expense: expense),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: expense.nextStatus.isNotEmpty
|
|
||||||
? SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
children: expense.nextStatus.map((next) {
|
|
||||||
return ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
minimumSize: const Size(100, 40),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8, horizontal: 12),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
final success =
|
|
||||||
await expenseController.updateExpenseStatus(
|
|
||||||
expense.id,
|
|
||||||
next.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
Get.snackbar(
|
|
||||||
'Success',
|
|
||||||
'Expense moved to ${next.name}',
|
|
||||||
backgroundColor: Colors.green.withOpacity(0.8),
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
Get.back(result: true);
|
|
||||||
} else {
|
|
||||||
Get.snackbar(
|
|
||||||
'Error',
|
|
||||||
'Failed to update status.',
|
|
||||||
backgroundColor: Colors.red.withOpacity(0.8),
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
next.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
: null,
|
}
|
||||||
|
|
||||||
|
final expense = controller.expense.value;
|
||||||
|
if (expense == null) {
|
||||||
|
return const Center(child: Text("No expense details found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
final statusColor = getStatusColor(
|
||||||
|
expense.status.name,
|
||||||
|
colorCode: expense.status.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
final formattedAmount = NumberFormat.currency(
|
||||||
|
locale: 'en_IN',
|
||||||
|
symbol: '₹ ',
|
||||||
|
decimalDigits: 2,
|
||||||
|
).format(expense.amount);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_ExpenseHeader(
|
||||||
|
title: expense.expensesType.name,
|
||||||
|
amount: formattedAmount,
|
||||||
|
status: expense.status.name,
|
||||||
|
statusColor: statusColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_ExpenseDetailsList(expense: expense),
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: Obx(() {
|
||||||
|
final expense = controller.expense.value;
|
||||||
|
if (expense == null || expense.nextStatus.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 10,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: expense.nextStatus.map((next) {
|
||||||
|
|
||||||
|
Color buttonColor = Colors.red;
|
||||||
|
if (next.color.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
buttonColor =
|
||||||
|
Color(int.parse(next.color.replaceFirst('#', '0xff')));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(100, 40),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||||
|
backgroundColor: buttonColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final success = await controller.updateExpenseStatus(
|
||||||
|
expense.id, next.id);
|
||||||
|
if (success) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Success',
|
||||||
|
'Expense moved to ${next.displayName.isNotEmpty ? next.displayName : next.name}',
|
||||||
|
backgroundColor: Colors.green.withOpacity(0.8),
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
await controller.fetchExpenseDetails(expenseId);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Failed to update status.',
|
||||||
|
backgroundColor: Colors.red.withOpacity(0.8),
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
next.displayName.isNotEmpty ? next.displayName : next.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading skeleton placeholder
|
||||||
|
Widget _buildLoadingSkeleton() {
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: List.generate(5, (index) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expense header card
|
||||||
class _ExpenseHeader extends StatelessWidget {
|
class _ExpenseHeader extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String amount;
|
final String amount;
|
||||||
@ -229,7 +295,7 @@ class _ExpenseHeader extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.15),
|
color: statusColor.withOpacity(0.15),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -239,8 +305,8 @@ class _ExpenseHeader extends StatelessWidget {
|
|||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
status,
|
status,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: statusColor,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -253,6 +319,7 @@ class _ExpenseHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expense details list
|
||||||
class _ExpenseDetailsList extends StatelessWidget {
|
class _ExpenseDetailsList extends StatelessWidget {
|
||||||
final ExpenseModel expense;
|
final ExpenseModel expense;
|
||||||
|
|
||||||
@ -289,28 +356,41 @@ class _ExpenseDetailsList extends StatelessWidget {
|
|||||||
_DetailRow(title: "Expense Type", value: expense.expensesType.name),
|
_DetailRow(title: "Expense Type", value: expense.expensesType.name),
|
||||||
_DetailRow(title: "Payment Mode", value: expense.paymentMode.name),
|
_DetailRow(title: "Payment Mode", value: expense.paymentMode.name),
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
title: "Paid By",
|
title: "Paid By",
|
||||||
value: '${expense.paidBy.firstName} ${expense.paidBy.lastName}'),
|
value: '${expense.paidBy.firstName} ${expense.paidBy.lastName}',
|
||||||
|
),
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
title: "Created By",
|
title: "Created By",
|
||||||
value:
|
value:
|
||||||
'${expense.createdBy.firstName} ${expense.createdBy.lastName}'),
|
'${expense.createdBy.firstName} ${expense.createdBy.lastName}',
|
||||||
|
),
|
||||||
_DetailRow(title: "Transaction Date", value: transactionDate),
|
_DetailRow(title: "Transaction Date", value: transactionDate),
|
||||||
_DetailRow(title: "Created At", value: createdAt),
|
_DetailRow(title: "Created At", value: createdAt),
|
||||||
_DetailRow(title: "Supplier Name", value: expense.supplerName),
|
_DetailRow(title: "Supplier Name", value: expense.supplerName),
|
||||||
_DetailRow(title: "Amount", value: '₹ ${expense.amount}'),
|
_DetailRow(
|
||||||
|
title: "Amount",
|
||||||
|
value: NumberFormat.currency(
|
||||||
|
locale: 'en_IN',
|
||||||
|
symbol: '₹ ',
|
||||||
|
decimalDigits: 2,
|
||||||
|
).format(expense.amount),
|
||||||
|
),
|
||||||
_DetailRow(title: "Status", value: expense.status.name),
|
_DetailRow(title: "Status", value: expense.status.name),
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
title: "Next Status",
|
title: "Next Status",
|
||||||
value: expense.nextStatus.map((e) => e.name).join(", ")),
|
value: expense.nextStatus.map((e) => e.name).join(", "),
|
||||||
|
),
|
||||||
_DetailRow(
|
_DetailRow(
|
||||||
title: "Pre-Approved", value: expense.preApproved ? "Yes" : "No"),
|
title: "Pre-Approved",
|
||||||
|
value: expense.preApproved ? "Yes" : "No",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A single row for expense details
|
||||||
class _DetailRow extends StatelessWidget {
|
class _DetailRow extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String value;
|
final String value;
|
||||||
@ -343,6 +423,7 @@ class _DetailRow extends StatelessWidget {
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
softWrap: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -5,178 +5,73 @@ import 'package:marco/helpers/utils/date_time_utils.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/model/employee_model.dart';
|
import 'package:marco/model/employee_model.dart';
|
||||||
|
|
||||||
|
/// Wrapper to open Expense Filter Bottom Sheet
|
||||||
|
void openExpenseFilterBottomSheet(
|
||||||
|
BuildContext context, ExpenseController expenseController) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) {
|
||||||
|
return ExpenseFilterBottomSheetWrapper(
|
||||||
|
expenseController: expenseController);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpenseFilterBottomSheetWrapper extends StatelessWidget {
|
||||||
|
final ExpenseController expenseController;
|
||||||
|
|
||||||
|
const ExpenseFilterBottomSheetWrapper(
|
||||||
|
{super.key, required this.expenseController});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.7,
|
||||||
|
minChildSize: 0.4,
|
||||||
|
maxChildSize: 0.95,
|
||||||
|
expand: false,
|
||||||
|
builder: (context, scrollController) {
|
||||||
|
return ExpenseFilterBottomSheet(
|
||||||
|
expenseController: expenseController,
|
||||||
|
scrollController: scrollController,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ExpenseFilterBottomSheet extends StatelessWidget {
|
class ExpenseFilterBottomSheet extends StatelessWidget {
|
||||||
final ExpenseController expenseController;
|
final ExpenseController expenseController;
|
||||||
final RxList<EmployeeModel> selectedPaidByEmployees;
|
final ScrollController scrollController;
|
||||||
final RxList<EmployeeModel> selectedCreatedByEmployees;
|
|
||||||
|
|
||||||
ExpenseFilterBottomSheet({
|
const ExpenseFilterBottomSheet({
|
||||||
super.key,
|
super.key,
|
||||||
required this.expenseController,
|
required this.expenseController,
|
||||||
required this.selectedPaidByEmployees,
|
required this.scrollController,
|
||||||
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
return Container(
|
return SafeArea(
|
||||||
padding: const EdgeInsets.all(16),
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
MyText.titleLarge('Filter Expenses', fontWeight: 700),
|
Expanded(
|
||||||
const SizedBox(height: 16),
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
/// Project Filter
|
padding:
|
||||||
MyText.bodyMedium('Project', fontWeight: 600),
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||||
const SizedBox(height: 6),
|
child: _buildContent(context),
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
_buildBottomButtons(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -184,17 +79,282 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Employee Filter Section
|
/// Builds the filter content
|
||||||
Widget _employeeFilterSection({
|
Widget _buildContent(BuildContext context) {
|
||||||
required String title,
|
return Column(
|
||||||
required RxList<EmployeeModel> selectedEmployees,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
required ExpenseController expenseController,
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 50,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
MyText.titleLarge('Filter Expenses', fontWeight: 700),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => expenseController.clearFilters(),
|
||||||
|
child: const Text(
|
||||||
|
"Reset Filter",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
/// Project Filter
|
||||||
|
_buildCardSection(
|
||||||
|
title: "Project",
|
||||||
|
child: _popupSelector(
|
||||||
|
context,
|
||||||
|
currentValue: expenseController.selectedProject.value.isEmpty
|
||||||
|
? 'Select Project'
|
||||||
|
: expenseController.selectedProject.value,
|
||||||
|
items: expenseController.globalProjects,
|
||||||
|
onSelected: (value) =>
|
||||||
|
expenseController.selectedProject.value = value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
/// Expense Status Filter
|
||||||
|
_buildCardSection(
|
||||||
|
title: "Expense Status",
|
||||||
|
child: _popupSelector(
|
||||||
|
context,
|
||||||
|
currentValue: expenseController.selectedStatus.value.isEmpty
|
||||||
|
? 'Select Expense Status'
|
||||||
|
: expenseController.expenseStatuses
|
||||||
|
.firstWhereOrNull((e) =>
|
||||||
|
e.id == expenseController.selectedStatus.value)
|
||||||
|
?.name ??
|
||||||
|
'Select Expense Status',
|
||||||
|
items:
|
||||||
|
expenseController.expenseStatuses.map((e) => e.name).toList(),
|
||||||
|
onSelected: (name) {
|
||||||
|
final status = expenseController.expenseStatuses
|
||||||
|
.firstWhere((e) => e.name == name);
|
||||||
|
expenseController.selectedStatus.value = status.id;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
/// Date Range Filter
|
||||||
|
_buildCardSection(
|
||||||
|
title: "Date Range",
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _dateButton(
|
||||||
|
label: expenseController.startDate.value == null
|
||||||
|
? 'Start Date'
|
||||||
|
: DateTimeUtils.formatDate(
|
||||||
|
expenseController.startDate.value!, 'dd MMM yyyy'),
|
||||||
|
onTap: () async {
|
||||||
|
DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate:
|
||||||
|
expenseController.startDate.value ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
if (picked != null)
|
||||||
|
expenseController.startDate.value = picked;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _dateButton(
|
||||||
|
label: expenseController.endDate.value == null
|
||||||
|
? 'End Date'
|
||||||
|
: DateTimeUtils.formatDate(
|
||||||
|
expenseController.endDate.value!, 'dd MMM yyyy'),
|
||||||
|
onTap: () async {
|
||||||
|
DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate:
|
||||||
|
expenseController.endDate.value ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
if (picked != null)
|
||||||
|
expenseController.endDate.value = picked;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
/// Paid By Filter
|
||||||
|
_buildCardSection(
|
||||||
|
title: "Paid By",
|
||||||
|
child: _employeeFilterSection(
|
||||||
|
selectedEmployees: expenseController.selectedPaidByEmployees,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
/// Created By Filter
|
||||||
|
_buildCardSection(
|
||||||
|
title: "Created By",
|
||||||
|
child: _employeeFilterSection(
|
||||||
|
selectedEmployees: expenseController.selectedCreatedByEmployees,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bottom Action Buttons
|
||||||
|
Widget _buildBottomButtons() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Cancel Button
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
label: MyText.bodyMedium(
|
||||||
|
"Cancel",
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Submit Button
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
expenseController.fetchExpenses();
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.check_circle_outline, color: Colors.white),
|
||||||
|
label: MyText.bodyMedium(
|
||||||
|
"Submit",
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.indigo,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Popup Selector
|
||||||
|
Widget _popupSelector(
|
||||||
|
BuildContext context, {
|
||||||
|
required String currentValue,
|
||||||
|
required List<String> items,
|
||||||
|
required ValueChanged<String> onSelected,
|
||||||
}) {
|
}) {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
onSelected: onSelected,
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return items
|
||||||
|
.map((e) => PopupMenuItem<String>(
|
||||||
|
value: e,
|
||||||
|
child: Text(e),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
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: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
currentValue,
|
||||||
|
style: const TextStyle(color: Colors.black87),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.arrow_drop_down, color: Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Card Section Wrapper
|
||||||
|
Widget _buildCardSection({required String title, required Widget child}) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodyMedium(title, fontWeight: 600),
|
MyText.bodyMedium(title, fontWeight: 600),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Date Button
|
||||||
|
Widget _dateButton({required String label, required VoidCallback onTap}) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: onTap,
|
||||||
|
icon: const Icon(Icons.calendar_today, size: 16),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
|
),
|
||||||
|
label: Text(label, overflow: TextOverflow.ellipsis),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Employee Filter Section
|
||||||
|
Widget _employeeFilterSection(
|
||||||
|
{required RxList<EmployeeModel> selectedEmployees}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
@ -270,35 +430,3 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,8 +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';
|
import 'package:marco/view/expense/expense_filter_bottom_sheet.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
||||||
|
|
||||||
class ExpenseMainScreen extends StatefulWidget {
|
class ExpenseMainScreen extends StatefulWidget {
|
||||||
const ExpenseMainScreen({super.key});
|
const ExpenseMainScreen({super.key});
|
||||||
@ -22,22 +22,33 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
final RxBool isHistoryView = false.obs;
|
final RxBool isHistoryView = false.obs;
|
||||||
final TextEditingController searchController = TextEditingController();
|
final TextEditingController searchController = TextEditingController();
|
||||||
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() {
|
||||||
super.initState();
|
super.initState();
|
||||||
expenseController.fetchExpenses();
|
expenseController.fetchExpenses(); // Initial data load
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refreshExpenses() {
|
void _refreshExpenses() {
|
||||||
expenseController.fetchExpenses();
|
expenseController.fetchExpenses();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openFilterBottomSheet(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) {
|
||||||
|
return ExpenseFilterBottomSheetWrapper(
|
||||||
|
expenseController: expenseController,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -49,14 +60,14 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
_SearchAndFilter(
|
_SearchAndFilter(
|
||||||
searchController: searchController,
|
searchController: searchController,
|
||||||
onChanged: (value) => searchQuery.value = value,
|
onChanged: (value) => searchQuery.value = value,
|
||||||
onFilterTap: _openFilterBottomSheet,
|
onFilterTap: () => _openFilterBottomSheet(context),
|
||||||
onRefreshTap: _refreshExpenses,
|
onRefreshTap: _refreshExpenses,
|
||||||
),
|
),
|
||||||
_ToggleButtons(isHistoryView: isHistoryView),
|
_ToggleButtons(isHistoryView: isHistoryView),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
if (expenseController.isLoading.value) {
|
if (expenseController.isLoading.value) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return SkeletonLoaders.expenseListSkeletonLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expenseController.errorMessage.isNotEmpty) {
|
if (expenseController.errorMessage.isNotEmpty) {
|
||||||
@ -81,7 +92,7 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
expense.paymentMode.name.toLowerCase().contains(query);
|
expense.paymentMode.name.toLowerCase().contains(query);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// Sort by latest transaction date first
|
// Sort by latest transaction date
|
||||||
filteredList.sort(
|
filteredList.sort(
|
||||||
(a, b) => b.transactionDate.compareTo(a.transactionDate));
|
(a, b) => b.transactionDate.compareTo(a.transactionDate));
|
||||||
|
|
||||||
@ -113,19 +124,9 @@ class _ExpenseMainScreenState extends State<ExpenseMainScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openFilterBottomSheet() {
|
|
||||||
Get.bottomSheet(
|
|
||||||
ExpenseFilterBottomSheet(
|
|
||||||
expenseController: expenseController,
|
|
||||||
selectedPaidByEmployees: selectedPaidByEmployees,
|
|
||||||
selectedCreatedByEmployees: selectedCreatedByEmployees,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppBar Widget
|
///---------------------- APP BAR ----------------------///
|
||||||
class _ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget {
|
class _ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final ProjectController projectController;
|
final ProjectController projectController;
|
||||||
|
|
||||||
@ -170,7 +171,6 @@ class _ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
projectController.selectedProject?.name ??
|
projectController.selectedProject?.name ??
|
||||||
'Select Project';
|
'Select Project';
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => Get.toNamed('/project-selector'),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.work_outline,
|
const Icon(Icons.work_outline,
|
||||||
@ -200,8 +200,7 @@ class _ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search and Filter Widget
|
///---------------------- SEARCH AND FILTER ----------------------///
|
||||||
|
|
||||||
class _SearchAndFilter extends StatelessWidget {
|
class _SearchAndFilter extends StatelessWidget {
|
||||||
final TextEditingController searchController;
|
final TextEditingController searchController;
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
@ -217,6 +216,8 @@ class _SearchAndFilter extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ExpenseController expenseController = Get.find<ExpenseController>();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: MySpacing.fromLTRB(12, 10, 12, 0),
|
padding: MySpacing.fromLTRB(12, 10, 12, 0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -259,17 +260,42 @@ class _SearchAndFilter extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
IconButton(
|
Obx(() {
|
||||||
icon: const Icon(Icons.tune, color: Colors.black),
|
final bool showRedDot = expenseController.isFilterApplied;
|
||||||
onPressed: onFilterTap,
|
return IconButton(
|
||||||
),
|
onPressed: onFilterTap,
|
||||||
|
icon: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.tune, color: Colors.black, size: 24),
|
||||||
|
if (showRedDot)
|
||||||
|
Positioned(
|
||||||
|
top: -1,
|
||||||
|
right: -1,
|
||||||
|
child: Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle Buttons Widget
|
///---------------------- TOGGLE BUTTONS ----------------------///
|
||||||
class _ToggleButtons extends StatelessWidget {
|
class _ToggleButtons extends StatelessWidget {
|
||||||
final RxBool isHistoryView;
|
final RxBool isHistoryView;
|
||||||
|
|
||||||
@ -360,7 +386,7 @@ class _ToggleButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expense List Widget (Dynamic)
|
///---------------------- EXPENSE LIST ----------------------///
|
||||||
class _ExpenseList extends StatelessWidget {
|
class _ExpenseList extends StatelessWidget {
|
||||||
final List<ExpenseModel> expenseList;
|
final List<ExpenseModel> expenseList;
|
||||||
|
|
||||||
@ -371,7 +397,7 @@ class _ExpenseList extends StatelessWidget {
|
|||||||
if (expenseList.isEmpty) {
|
if (expenseList.isEmpty) {
|
||||||
return Center(child: MyText.bodyMedium('No expenses found.'));
|
return Center(child: MyText.bodyMedium('No expenses found.'));
|
||||||
}
|
}
|
||||||
|
final expenseController = Get.find<ExpenseController>();
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
||||||
itemCount: expenseList.length,
|
itemCount: expenseList.length,
|
||||||
@ -386,10 +412,17 @@ class _ExpenseList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Get.to(
|
onTap: () async {
|
||||||
() => const ExpenseDetailScreen(),
|
final result = await Get.to(
|
||||||
arguments: {'expense': expense},
|
() => ExpenseDetailScreen(expenseId: expense.id),
|
||||||
),
|
arguments: {'expense': expense},
|
||||||
|
);
|
||||||
|
|
||||||
|
// If status was updated, refresh expenses
|
||||||
|
if (result == true) {
|
||||||
|
expenseController.fetchExpenses();
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -398,13 +431,9 @@ class _ExpenseList extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
MyText.bodyMedium(
|
||||||
children: [
|
expense.expensesType.name,
|
||||||
MyText.bodyMedium(
|
fontWeight: 700,
|
||||||
expense.expensesType.name,
|
|
||||||
fontWeight: 700,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
MyText.bodyMedium(
|
MyText.bodyMedium(
|
||||||
'₹ ${expense.amount.toStringAsFixed(2)}',
|
'₹ ${expense.amount.toStringAsFixed(2)}',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user