diff --git a/lib/controller/finance/payment_request_detail_controller.dart b/lib/controller/finance/payment_request_detail_controller.dart index bb3421a..65e455c 100644 --- a/lib/controller/finance/payment_request_detail_controller.dart +++ b/lib/controller/finance/payment_request_detail_controller.dart @@ -327,7 +327,8 @@ class PaymentRequestDetailController extends GetxController { } // --- Submit Expense --- - Future submitExpense() async { + Future submitExpense( + {required String statusId, String? comment}) async { if (selectedPaymentMode.value == null) return false; isSubmitting.value = true; @@ -355,6 +356,8 @@ class PaymentRequestDetailController extends GetxController { gstNumber: gstNumberController.text, paymentRequestId: _requestId, billAttachments: attachmentsPayload, + statusId: statusId, + comment: comment ?? '', ); } finally { isSubmitting.value = false; diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index b5ae4b0..9c4c59a 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -22,7 +22,7 @@ class ApiEndpoints { static const String updateExpensePaymentRequestStatus = "/Expense/payment-request/action"; static const String createExpenseforPR = - "/Expense/payment-request/expense/create"; + "/expense/payment-request/action"; static const String getDashboardProjectProgress = "/dashboard/progression"; @@ -96,7 +96,7 @@ class ApiEndpoints { static const String editExpense = "/Expense/edit"; static const String getMasterPaymentModes = "/master/payment-modes"; static const String getMasterExpenseStatus = "/master/expenses-status"; - static const String getMasterExpenseTypes = "/master/expenses-types"; + static const String getMasterExpenseCategory = "/master/expenses-categories"; static const String updateExpenseStatus = "/expense/action"; static const String deleteExpense = "/expense/delete"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 6c1d273..a413c4f 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -307,7 +307,9 @@ class ApiService { required String paymentModeId, required String location, required String gstNumber, + required String statusId, required String paymentRequestId, + required String comment, List> billAttachments = const [], }) async { const endpoint = ApiEndpoints.createExpenseforPR; @@ -316,6 +318,8 @@ class ApiService { "paymentModeId": paymentModeId, "location": location, "gstNumber": gstNumber, + "statusId": statusId, + "comment": comment, "paymentRequestId": paymentRequestId, "billAttachments": billAttachments, }; @@ -789,7 +793,8 @@ class ApiService { return null; } -/// Create Project API + + /// Create Project API static Future createProjectApi({ required String name, required String projectAddress, @@ -1830,7 +1835,7 @@ class ApiService { /// Fetch Master Expense Types static Future?> getMasterExpenseTypes() async { - const endpoint = ApiEndpoints.getMasterExpenseTypes; + const endpoint = ApiEndpoints.getMasterExpenseCategory; return _getRequest(endpoint).then((res) => res != null ? _parseResponse(res, label: 'Master Expense Types') : null); diff --git a/lib/helpers/widgets/expense/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart index d843759..9e96931 100644 --- a/lib/helpers/widgets/expense/expense_main_components.dart +++ b/lib/helpers/widgets/expense/expense_main_components.dart @@ -9,6 +9,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/model/expense/expense_list_model.dart'; import 'package:marco/view/expense/expense_detail_screen.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; class ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget { final ProjectController projectController; @@ -72,7 +73,7 @@ class ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget { } } -class SearchAndFilter extends StatelessWidget { +class SearchAndFilter extends StatefulWidget { final TextEditingController controller; final ValueChanged onChanged; final VoidCallback onFilterTap; @@ -86,6 +87,11 @@ class SearchAndFilter extends StatelessWidget { super.key, }); + @override + State createState() => _SearchAndFilterState(); +} + +class _SearchAndFilterState extends State with UIMixin { @override Widget build(BuildContext context) { return Padding( @@ -96,8 +102,8 @@ class SearchAndFilter extends StatelessWidget { child: SizedBox( height: 35, child: TextField( - controller: controller, - onChanged: onChanged, + controller: widget.controller, + onChanged: widget.onChanged, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12), prefixIcon: @@ -106,11 +112,16 @@ class SearchAndFilter extends StatelessWidget { filled: true, fillColor: Colors.white, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: + BorderSide(color: contentTheme.primary, width: 1.5), + ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), @@ -124,7 +135,7 @@ class SearchAndFilter extends StatelessWidget { clipBehavior: Clip.none, children: [ const Icon(Icons.tune, color: Colors.black), - if (expenseController.isFilterApplied) + if (widget.expenseController.isFilterApplied) Positioned( top: -1, right: -1, @@ -140,7 +151,7 @@ class SearchAndFilter extends StatelessWidget { ), ], ), - onPressed: onFilterTap, + onPressed: widget.onFilterTap, ); }), ], diff --git a/lib/model/finance/make_expense_bottom_sheet.dart b/lib/model/finance/make_expense_bottom_sheet.dart index 5652ff6..54cc8fe 100644 --- a/lib/model/finance/make_expense_bottom_sheet.dart +++ b/lib/model/finance/make_expense_bottom_sheet.dart @@ -8,14 +8,18 @@ import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; import 'package:marco/helpers/utils/validators.dart'; import 'package:marco/controller/finance/payment_request_detail_controller.dart'; -Future showCreateExpenseBottomSheet() { +Future showCreateExpenseBottomSheet({required String statusId}) { return Get.bottomSheet( - _CreateExpenseBottomSheet(), + _CreateExpenseBottomSheet(statusId: statusId), isScrollControlled: true, ); } class _CreateExpenseBottomSheet extends StatefulWidget { + final String statusId; + + const _CreateExpenseBottomSheet({required this.statusId, Key? key}) + : super(key: key); @override State<_CreateExpenseBottomSheet> createState() => _CreateExpenseBottomSheetState(); @@ -24,8 +28,9 @@ class _CreateExpenseBottomSheet extends StatefulWidget { class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { final controller = Get.put(PaymentRequestDetailController()); final _formKey = GlobalKey(); - final _paymentModeDropdownKey = GlobalKey(); + final TextEditingController commentController = TextEditingController(); + final _paymentModeDropdownKey = GlobalKey(); @override Widget build(BuildContext context) { return Obx( @@ -37,7 +42,10 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { onCancel: Get.back, onSubmit: () async { if (_formKey.currentState!.validate() && _validateSelections()) { - final success = await controller.submitExpense(); + final success = await controller.submitExpense( + statusId: widget.statusId, + comment: commentController.text.trim(), + ); if (success) { Get.back(); showAppSnackbar( @@ -47,6 +55,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { ); } } + ; }, child: SingleChildScrollView( child: Column( @@ -67,7 +76,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { Icons.receipt_outlined, controller.gstNumberController, hint: "Enter GST Number", - validator: null, // optional field + validator: null, ), _gap(), _buildTextField( @@ -86,6 +95,15 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { ), _gap(), _buildAttachmentField(), + _gap(), + _buildTextField( + "Comment", + Icons.comment_outlined, + commentController, + hint: "Enter a comment (optional)", + validator: null, + ), + _gap(), ], ), ), @@ -131,7 +149,7 @@ class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { hint: hint ?? "", validator: validator, keyboardType: keyboardType ?? TextInputType.text, - suffixIcon: suffixIcon, + suffixIcon: suffixIcon, ), ], ); diff --git a/lib/model/finance/payment_request_details_model.dart b/lib/model/finance/payment_request_details_model.dart index a1c9820..6a1d899 100644 --- a/lib/model/finance/payment_request_details_model.dart +++ b/lib/model/finance/payment_request_details_model.dart @@ -118,8 +118,7 @@ class PaymentRequestData { expenseStatus: ExpenseStatus.fromJson(json['expenseStatus']), paidTransactionId: json['paidTransactionId'], paidAt: json['paidAt'] != null ? DateTime.parse(json['paidAt']) : null, - paidBy: - json['paidBy'] != null ? User.fromJson(json['paidBy']) : null, + paidBy: json['paidBy'] != null ? User.fromJson(json['paidBy']) : null, isAdvancePayment: json['isAdvancePayment'], createdAt: DateTime.parse(json['createdAt']), createdBy: User.fromJson(json['createdBy']), @@ -373,7 +372,7 @@ class NextStatus { class UpdateLog { String id; - ExpenseStatus status; + ExpenseStatus? status; ExpenseStatus nextStatus; String comment; DateTime updatedAt; @@ -381,7 +380,7 @@ class UpdateLog { UpdateLog({ required this.id, - required this.status, + this.status, required this.nextStatus, required this.comment, required this.updatedAt, @@ -390,7 +389,9 @@ class UpdateLog { factory UpdateLog.fromJson(Map json) => UpdateLog( id: json['id'], - status: ExpenseStatus.fromJson(json['status']), + status: json['status'] != null + ? ExpenseStatus.fromJson(json['status']) + : null, nextStatus: ExpenseStatus.fromJson(json['nextStatus']), comment: json['comment'], updatedAt: DateTime.parse(json['updatedAt']), @@ -399,7 +400,7 @@ class UpdateLog { Map toJson() => { 'id': id, - 'status': status.toJson(), + 'status': status?.toJson(), 'nextStatus': nextStatus.toJson(), 'comment': comment, 'updatedAt': updatedAt.toIso8601String(), diff --git a/lib/view/finance/payment_request_detail_screen.dart b/lib/view/finance/payment_request_detail_screen.dart index f9b86a2..8945252 100644 --- a/lib/view/finance/payment_request_detail_screen.dart +++ b/lib/view/finance/payment_request_detail_screen.dart @@ -34,7 +34,7 @@ class _PaymentRequestDetailScreenState extends State with UIMixin { final controller = Get.put(PaymentRequestDetailController()); final projectController = Get.find(); - final permissionController = Get.find(); + final permissionController = Get.put(PermissionController()); final RxBool canSubmit = false.obs; bool _checkedPermission = false; @@ -163,36 +163,6 @@ class _PaymentRequestDetailScreenState extends State .hasAnyPermission(status.permissionIds ?? []); }).toList(); - // If there are no next statuses, show "Create Expense" button - if (availableStatuses.isEmpty) { - return SafeArea( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), - ), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () { - showCreateExpenseBottomSheet(); - }, - child: const Text( - "Create Expense", - style: TextStyle( - color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - ), - ); - } - // Normal status buttons return SafeArea( child: Container( @@ -218,6 +188,7 @@ class _PaymentRequestDetailScreenState extends State ), ), onPressed: () async { + // If status is reimbursement, show reimbursement bottom sheet if (status.id == reimbursementStatusId) { showModalBottomSheet( context: context, @@ -232,6 +203,15 @@ class _PaymentRequestDetailScreenState extends State onClose: () {}, ), ); + + // If status is b8586f67-dc19-49c3-b4af-224149efe1d3, open create expense + } else if (status.id == + 'b8586f67-dc19-49c3-b4af-224149efe1d3') { + showCreateExpenseBottomSheet( + statusId: status.id, + ); + + // Normal status flow } else { final comment = await showCommentBottomSheet( context, status.displayName); @@ -407,8 +387,12 @@ class _Logs extends StatelessWidget { itemBuilder: (_, index) { final log = reversedLogs[index]; - final status = log.status.name; - final description = log.status.description; + final status = log.status?.name ?? 'Unknown'; + final description = log.status?.description ?? ''; + final statusColor = log.status != null + ? colorParser(log.status!.color) + : Colors.grey; + final comment = log.comment; final nextStatusName = log.nextStatus.name; @@ -421,7 +405,6 @@ class _Logs extends StatelessWidget { final timestamp = _parseTimestamp(log.updatedAt); final timeAgo = timeago.format(timestamp); - final statusColor = colorParser(log.status.color); final nextStatusColor = colorParser(log.nextStatus.color); return TimelineTile(