diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 72f2331..6eb76f3 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -3,6 +3,7 @@ import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/model/dashboard/project_progress_model.dart'; +import 'package:marco/model/dashboard/pending_expenses_model.dart'; class DashboardController extends GetxController { // ========================= @@ -48,7 +49,11 @@ class DashboardController extends GetxController { // Inject ProjectController final ProjectController projectController = Get.find(); - +// Pending Expenses overview +// ========================= + final RxBool isPendingExpensesLoading = false.obs; + final Rx pendingExpensesData = + Rx(null); @override void onInit() { super.onInit(); @@ -147,9 +152,35 @@ class DashboardController extends GetxController { fetchProjectProgress(), fetchDashboardTasks(projectId: projectId), fetchDashboardTeams(projectId: projectId), + fetchPendingExpenses(), ]); } + Future fetchPendingExpenses() async { + final String projectId = projectController.selectedProjectId.value; + if (projectId.isEmpty) return; + + try { + isPendingExpensesLoading.value = true; + final response = + await ApiService.getPendingExpensesApi(projectId: projectId); + + if (response != null && response.success) { + pendingExpensesData.value = response.data; + logSafe('Pending expenses fetched successfully.', level: LogLevel.info); + } else { + pendingExpensesData.value = null; + logSafe('Failed to fetch pending expenses.', level: LogLevel.error); + } + } catch (e, st) { + pendingExpensesData.value = null; + logSafe('Error fetching pending expenses', + level: LogLevel.error, error: e, stackTrace: st); + } finally { + isPendingExpensesLoading.value = false; + } + } + // ========================= // API Calls // ========================= diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index eea7ae8..5029399 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,5 +1,6 @@ class ApiEndpoints { - static const String baseUrl = "https://mapi.marcoaiot.com/api"; + // static const String baseUrl = "https://mapi.marcoaiot.com/api"; + static const String baseUrl = "https://ofwapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; @@ -10,12 +11,14 @@ class ApiEndpoints { static const String getDashboardTasks = "/dashboard/tasks"; static const String getDashboardTeams = "/dashboard/teams"; static const String getDashboardProjects = "/dashboard/projects"; + static const String getDashboardMonthlyExpenses = + "/Dashboard/expense/monthly"; + static const String getExpenseTypeReport = "/Dashboard/expense/type"; + static const String getPendingExpenses = "/Dashboard/expense/pendings"; ///// Projects Module API Endpoints static const String createProject = "/project"; - - // Attendance Module API Endpoints static const String getProjects = "/project/list"; static const String getGlobalProjects = "/project/list/basic"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 8fed938..581c0e2 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -19,13 +19,14 @@ import 'package:marco/model/document/master_document_type_model.dart'; import 'package:marco/model/document/document_details_model.dart'; import 'package:marco/model/document/document_version_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; +import 'package:marco/model/dashboard/pending_expenses_model.dart'; class ApiService { static const bool enableLogs = true; static const Duration extendedTimeout = Duration(seconds: 60); static Future _getToken() async { - final token = await LocalStorage.getJwtToken(); + final token = LocalStorage.getJwtToken(); if (token == null) { logSafe("No JWT token found. Logging out..."); @@ -38,7 +39,7 @@ class ApiService { logSafe("Access token is expired. Attempting refresh..."); final refreshed = await AuthService.refreshToken(); if (refreshed) { - return await LocalStorage.getJwtToken(); + return LocalStorage.getJwtToken(); } else { logSafe("Token refresh failed. Logging out immediately..."); await LocalStorage.logout(); @@ -55,7 +56,7 @@ class ApiService { "Access token is about to expire in ${difference.inSeconds}s. Refreshing..."); final refreshed = await AuthService.refreshToken(); if (refreshed) { - return await LocalStorage.getJwtToken(); + return LocalStorage.getJwtToken(); } else { logSafe("Token refresh failed (near expiry). Logging out..."); await LocalStorage.logout(); @@ -288,6 +289,39 @@ class ApiService { } } + /// Get Pending Expenses + static Future getPendingExpensesApi({ + required String projectId, + }) async { + const endpoint = ApiEndpoints.getPendingExpenses; + logSafe("Fetching Pending Expenses for projectId: $projectId"); + + try { + final response = await _getRequest( + endpoint, + queryParams: {'projectId': projectId}, + ); + + if (response == null) { + logSafe("Pending Expenses request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Pending Expenses"); + + if (jsonResponse != null) { + return PendingExpensesResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getPendingExpensesApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } /// Create Project API static Future createProjectApi({ diff --git a/lib/helpers/widgets/dashbaord/expense_by_status_widget.dart b/lib/helpers/widgets/dashbaord/expense_by_status_widget.dart new file mode 100644 index 0000000..f3406ea --- /dev/null +++ b/lib/helpers/widgets/dashbaord/expense_by_status_widget.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class ExpenseByStatusWidget extends StatelessWidget { + final DashboardController controller; + + const ExpenseByStatusWidget({super.key, required this.controller}); + + Widget _buildStatusTile({ + required IconData icon, + required Color color, + required String title, + required String amount, + required String count, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + CircleAvatar( + backgroundColor: Colors.white, + radius: 20, + child: Icon(icon, color: color, size: 22), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium( + title, + fontWeight: 600, + ), + const SizedBox(height: 2), + MyText.bodyMedium( + amount, + color: Colors.blue, + ), + ], + ), + ), + MyText.titleMedium( + count, + color: Colors.blue, + fontWeight: 600, + ), + const Icon(Icons.chevron_right, color: Colors.blue, size: 22), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Obx(() { + final data = controller.pendingExpensesData.value; + + if (controller.isPendingExpensesLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (data == null) { + return Center( + child: MyText.bodyMedium("No expense status data available"), + ); + } + + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleLarge( + "Expense - By Status", + fontWeight: 700, + ), + const SizedBox(height: 4), + MyText.bodyMedium( + controller.projectController.selectedProjectName.value, + color: Colors.grey.shade600, + ), + const SizedBox(height: 16), + + // Pending Payment + _buildStatusTile( + icon: Icons.currency_rupee, + color: Colors.blue, + title: "Pending Payment", + amount: "₹${data.processPending.amount.toStringAsFixed(1)}K", + count: data.processPending.count.toString(), + ), + + // Pending Approve + _buildStatusTile( + icon: Icons.check_circle_outline, + color: Colors.orange, + title: "Pending Approve", + amount: "₹${data.approvePending.amount.toStringAsFixed(1)}K", + count: data.approvePending.count.toString(), + ), + + // Pending Review + _buildStatusTile( + icon: Icons.search, + color: Colors.grey.shade700, + title: "Pending Review", + amount: "₹${data.reviewPending.amount.toStringAsFixed(1)}K", + count: data.reviewPending.count.toString(), + ), + + // Draft + _buildStatusTile( + icon: Icons.insert_drive_file_outlined, + color: Colors.cyan, + title: "Draft", + amount: "₹${data.draft.amount.toStringAsFixed(1)}K", + count: data.draft.count.toString(), + ), + + const SizedBox(height: 8), + Divider(color: Colors.grey.shade300), + const SizedBox(height: 8), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium( + "Project Spendings:", + fontWeight: 600, + ), + MyText.bodySmall( + "(All Processed Payments)", + color: Colors.grey.shade600, + ), + ], + ), + MyText.titleLarge( + "₹${(data.totalAmount / 1000).toStringAsFixed(2)}K >", + color: Colors.blue, + fontWeight: 700, + ), + ], + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/model/dashboard/expense_report_response_model.dart b/lib/model/dashboard/expense_report_response_model.dart new file mode 100644 index 0000000..4887ac4 --- /dev/null +++ b/lib/model/dashboard/expense_report_response_model.dart @@ -0,0 +1,74 @@ +class ExpenseReportResponse { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + ExpenseReportResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ExpenseReportResponse.fromJson(Map json) { + return ExpenseReportResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null + ? List.from( + json['data'].map((x) => ExpenseReportData.fromJson(x))) + : [], + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp']), + ); + } + + Map toJson() => { + 'success': success, + 'message': message, + 'data': data.map((x) => x.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; +} + +class ExpenseReportData { + final String monthName; + final int year; + final double total; + final int count; + + ExpenseReportData({ + required this.monthName, + required this.year, + required this.total, + required this.count, + }); + + factory ExpenseReportData.fromJson(Map json) { + return ExpenseReportData( + monthName: json['monthName'] ?? '', + year: json['year'] ?? 0, + total: json['total'] != null + ? (json['total'] is int + ? (json['total'] as int).toDouble() + : json['total'] as double) + : 0.0, + count: json['count'] ?? 0, + ); + } + + Map toJson() => { + 'monthName': monthName, + 'year': year, + 'total': total, + 'count': count, + }; +} diff --git a/lib/model/dashboard/expense_type_report_model.dart b/lib/model/dashboard/expense_type_report_model.dart new file mode 100644 index 0000000..7620ee0 --- /dev/null +++ b/lib/model/dashboard/expense_type_report_model.dart @@ -0,0 +1,105 @@ +class ExpenseTypeReportResponse { + final bool success; + final String message; + final ExpenseTypeReportData data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + ExpenseTypeReportResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ExpenseTypeReportResponse.fromJson(Map json) { + return ExpenseTypeReportResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: ExpenseTypeReportData.fromJson(json['data'] ?? {}), + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp']), + ); + } + + Map toJson() => { + 'success': success, + 'message': message, + 'data': data.toJson(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; +} + +class ExpenseTypeReportData { + final List report; + final double totalAmount; + + ExpenseTypeReportData({ + required this.report, + required this.totalAmount, + }); + + factory ExpenseTypeReportData.fromJson(Map json) { + return ExpenseTypeReportData( + report: json['report'] != null + ? List.from( + json['report'].map((x) => ExpenseTypeReportItem.fromJson(x))) + : [], + totalAmount: json['totalAmount'] != null + ? (json['totalAmount'] is int + ? (json['totalAmount'] as int).toDouble() + : json['totalAmount'] as double) + : 0.0, + ); + } + + Map toJson() => { + 'report': report.map((x) => x.toJson()).toList(), + 'totalAmount': totalAmount, + }; +} + +class ExpenseTypeReportItem { + final String projectName; + final double totalApprovedAmount; + final double totalPendingAmount; + final double totalRejectedAmount; + final double totalProcessedAmount; + + ExpenseTypeReportItem({ + required this.projectName, + required this.totalApprovedAmount, + required this.totalPendingAmount, + required this.totalRejectedAmount, + required this.totalProcessedAmount, + }); + + factory ExpenseTypeReportItem.fromJson(Map json) { + double parseAmount(dynamic value) { + if (value == null) return 0.0; + return value is int ? value.toDouble() : value as double; + } + + return ExpenseTypeReportItem( + projectName: json['projectName'] ?? '', + totalApprovedAmount: parseAmount(json['totalApprovedAmount']), + totalPendingAmount: parseAmount(json['totalPendingAmount']), + totalRejectedAmount: parseAmount(json['totalRejectedAmount']), + totalProcessedAmount: parseAmount(json['totalProcessedAmount']), + ); + } + + Map toJson() => { + 'projectName': projectName, + 'totalApprovedAmount': totalApprovedAmount, + 'totalPendingAmount': totalPendingAmount, + 'totalRejectedAmount': totalRejectedAmount, + 'totalProcessedAmount': totalProcessedAmount, + }; +} diff --git a/lib/model/dashboard/master_expense_types_model.dart b/lib/model/dashboard/master_expense_types_model.dart new file mode 100644 index 0000000..cd569ab --- /dev/null +++ b/lib/model/dashboard/master_expense_types_model.dart @@ -0,0 +1,74 @@ +class ExpenseTypeResponse { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + ExpenseTypeResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ExpenseTypeResponse.fromJson(Map json) { + return ExpenseTypeResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null + ? List.from( + json['data'].map((x) => ExpenseTypeData.fromJson(x))) + : [], + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp']), + ); + } + + Map toJson() => { + 'success': success, + 'message': message, + 'data': data.map((x) => x.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; +} + +class ExpenseTypeData { + final String id; + final String name; + final bool noOfPersonsRequired; + final bool isAttachmentRequried; + final String description; + + ExpenseTypeData({ + required this.id, + required this.name, + required this.noOfPersonsRequired, + required this.isAttachmentRequried, + required this.description, + }); + + factory ExpenseTypeData.fromJson(Map json) { + return ExpenseTypeData( + id: json['id'] ?? '', + name: json['name'] ?? '', + noOfPersonsRequired: json['noOfPersonsRequired'] ?? false, + isAttachmentRequried: json['isAttachmentRequried'] ?? false, + description: json['description'] ?? '', + ); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'noOfPersonsRequired': noOfPersonsRequired, + 'isAttachmentRequried': isAttachmentRequried, + 'description': description, + }; +} diff --git a/lib/model/dashboard/pending_expenses_model.dart b/lib/model/dashboard/pending_expenses_model.dart new file mode 100644 index 0000000..0f59826 --- /dev/null +++ b/lib/model/dashboard/pending_expenses_model.dart @@ -0,0 +1,169 @@ +import 'package:equatable/equatable.dart'; + +class PendingExpensesResponse extends Equatable { + final bool success; + final String message; + final PendingExpensesData? data; + final dynamic errors; + final int statusCode; + final String timestamp; + + const PendingExpensesResponse({ + required this.success, + required this.message, + this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory PendingExpensesResponse.fromJson(Map json) { + return PendingExpensesResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null + ? PendingExpensesData.fromJson(json['data']) + : null, + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: json['timestamp'] ?? '', + ); + } + + Map toJson() { + return { + 'success': success, + 'message': message, + 'data': data?.toJson(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp, + }; + } + + PendingExpensesResponse copyWith({ + bool? success, + String? message, + PendingExpensesData? data, + dynamic errors, + int? statusCode, + String? timestamp, + }) { + return PendingExpensesResponse( + success: success ?? this.success, + message: message ?? this.message, + data: data ?? this.data, + errors: errors ?? this.errors, + statusCode: statusCode ?? this.statusCode, + timestamp: timestamp ?? this.timestamp, + ); + } + + @override + List get props => [success, message, data, errors, statusCode, timestamp]; +} + +class PendingExpensesData extends Equatable { + final ExpenseStatus draft; + final ExpenseStatus reviewPending; + final ExpenseStatus approvePending; + final ExpenseStatus processPending; + final ExpenseStatus submited; + final double totalAmount; + + const PendingExpensesData({ + required this.draft, + required this.reviewPending, + required this.approvePending, + required this.processPending, + required this.submited, + required this.totalAmount, + }); + + factory PendingExpensesData.fromJson(Map json) { + return PendingExpensesData( + draft: ExpenseStatus.fromJson(json['draft']), + reviewPending: ExpenseStatus.fromJson(json['reviewPending']), + approvePending: ExpenseStatus.fromJson(json['approvePending']), + processPending: ExpenseStatus.fromJson(json['processPending']), + submited: ExpenseStatus.fromJson(json['submited']), + totalAmount: (json['totalAmount'] ?? 0).toDouble(), + ); + } + + Map toJson() { + return { + 'draft': draft.toJson(), + 'reviewPending': reviewPending.toJson(), + 'approvePending': approvePending.toJson(), + 'processPending': processPending.toJson(), + 'submited': submited.toJson(), + 'totalAmount': totalAmount, + }; + } + + PendingExpensesData copyWith({ + ExpenseStatus? draft, + ExpenseStatus? reviewPending, + ExpenseStatus? approvePending, + ExpenseStatus? processPending, + ExpenseStatus? submited, + double? totalAmount, + }) { + return PendingExpensesData( + draft: draft ?? this.draft, + reviewPending: reviewPending ?? this.reviewPending, + approvePending: approvePending ?? this.approvePending, + processPending: processPending ?? this.processPending, + submited: submited ?? this.submited, + totalAmount: totalAmount ?? this.totalAmount, + ); + } + + @override + List get props => [ + draft, + reviewPending, + approvePending, + processPending, + submited, + totalAmount, + ]; +} + +class ExpenseStatus extends Equatable { + final int count; + final double totalAmount; + + const ExpenseStatus({ + required this.count, + required this.totalAmount, + }); + + factory ExpenseStatus.fromJson(Map json) { + return ExpenseStatus( + count: json['count'] ?? 0, + totalAmount: (json['totalAmount'] ?? 0).toDouble(), + ); + } + + Map toJson() { + return { + 'count': count, + 'totalAmount': totalAmount, + }; + } + + ExpenseStatus copyWith({ + int? count, + double? totalAmount, + }) { + return ExpenseStatus( + count: count ?? this.count, + totalAmount: totalAmount ?? this.totalAmount, + ); + } + + @override + List get props => [count, totalAmount]; +} diff --git a/pubspec.yaml b/pubspec.yaml index f7e01f2..9e05a09 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -80,6 +80,7 @@ dependencies: googleapis_auth: ^2.0.0 device_info_plus: ^11.3.0 flutter_local_notifications: 19.4.0 + equatable: ^2.0.7 timeline_tile: ^2.0.0 dev_dependencies: