From fd57686c8a765f1cc76c7fb4a3abaa732893c31d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 10 Nov 2025 12:57:32 +0530 Subject: [PATCH] corrected the attachment issue --- .../add_payment_request_controller.dart | 22 ++ .../expense/add_expense_bottom_sheet.dart | 1 - .../add_payment_request_bottom_sheet.dart | 98 +++++- .../payment_request_detail_screen.dart | 288 +++++++++++------- 4 files changed, 294 insertions(+), 115 deletions(-) diff --git a/lib/controller/finance/add_payment_request_controller.dart b/lib/controller/finance/add_payment_request_controller.dart index 0381981..475eaf4 100644 --- a/lib/controller/finance/add_payment_request_controller.dart +++ b/lib/controller/finance/add_payment_request_controller.dart @@ -156,6 +156,28 @@ class AddPaymentRequestController extends GetxController { } } + Future pickFromCamera() async { + try { + final pickedFile = await _picker.pickImage(source: ImageSource.camera); + if (pickedFile != null) { + isProcessingAttachment.value = true; + File imageFile = File(pickedFile.path); + + // Add timestamp to the captured image + File timestampedFile = await TimestampImageHelper.addTimestamp( + imageFile: imageFile, + ); + + attachments.add(timestampedFile); + attachments.refresh(); // refresh UI + } + } catch (e) { + _errorSnackbar("Camera error: $e"); + } finally { + isProcessingAttachment.value = false; // stop loading + } + } + /// Selection handlers void selectProject(Map project) => selectedProject.value = project; diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart index 68e3f9d..317bf9b 100644 --- a/lib/model/expense/add_expense_bottom_sheet.dart +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -479,7 +479,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> message: 'Attachment has been removed.', type: SnackbarType.success, ); - Navigator.pop(context); }, ), ); diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index 07a0db5..c7cc543 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -1,4 +1,3 @@ -// payment_request_bottom_sheet.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/finance/add_payment_request_controller.dart'; @@ -10,16 +9,31 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; -Future showPaymentRequestBottomSheet({bool isEdit = false}) { +Future showPaymentRequestBottomSheet({ + bool isEdit = false, + Map? existingData, + VoidCallback? onUpdated, +}) { return Get.bottomSheet( - _PaymentRequestBottomSheet(isEdit: isEdit), + _PaymentRequestBottomSheet( + isEdit: isEdit, + existingData: existingData, + onUpdated: onUpdated, + ), isScrollControlled: true, ); } class _PaymentRequestBottomSheet extends StatefulWidget { final bool isEdit; - const _PaymentRequestBottomSheet({this.isEdit = false}); + final Map? existingData; + final VoidCallback? onUpdated; + + const _PaymentRequestBottomSheet({ + this.isEdit = false, + this.existingData, + this.onUpdated, + }); @override State<_PaymentRequestBottomSheet> createState() => @@ -35,6 +49,64 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> final _categoryDropdownKey = GlobalKey(); final _currencyDropdownKey = GlobalKey(); + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (widget.isEdit && widget.existingData != null) { + final data = widget.existingData!; + + // 🧩 Prefill basic text fields + controller.titleController.text = data["title"] ?? ""; + controller.amountController.text = data["amount"]?.toString() ?? ""; + controller.descriptionController.text = data["description"] ?? ""; + controller.dueDateController.text = + data["dueDate"]?.toString().split(" ")[0] ?? ""; + + // 🧩 Prefill dropdowns & toggles + controller.selectedProject.value = { + 'id': data["projectId"], + 'name': data["projectName"], + }; + controller.selectedPayee.value = data["payee"] ?? ""; + controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; + + // 🕒 Wait until categories & currencies are loaded before setting them + everAll([ + controller.categories, + controller.currencies, + ], (_) { + controller.selectedCategory.value = controller.categories + .firstWhereOrNull((c) => c.id == data["expenseCategoryId"]); + controller.selectedCurrency.value = controller.currencies + .firstWhereOrNull((c) => c.id == data["currencyId"]); + }); + + // 🖇 Attachments - Safe parsing (avoids null or wrong type) + final attachmentsData = data["attachments"]; + if (attachmentsData != null && + attachmentsData is List && + attachmentsData.isNotEmpty) { + final attachments = attachmentsData + .whereType>() + .map((a) => { + "id": a["id"], + "fileName": a["fileName"], + "url": a["url"], + "thumbUrl": a["thumbUrl"], + "fileSize": a["fileSize"] ?? 0, + "contentType": a["contentType"] ?? "", + }) + .toList(); + controller.existingAttachments.assignAll(attachments); + } else { + controller.existingAttachments.clear(); + } + } + }); + } + @override Widget build(BuildContext context) { return Obx(() => Form( @@ -49,12 +121,14 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (_formKey.currentState!.validate() && _validateSelections()) { final success = await controller.submitPaymentRequest(); if (success) { - // First close the BottomSheet Get.back(); - // Then show Snackbar + if (widget.onUpdated != null) widget.onUpdated!(); + showAppSnackbar( title: "Success", - message: "Payment request created successfully!", + message: widget.isEdit + ? "Payment request updated successfully!" + : "Payment request created successfully!", type: SnackbarType.success, ); } @@ -360,7 +434,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> title: 'Removed', message: 'Attachment has been removed.', type: SnackbarType.success); - Navigator.pop(context); }, ), ); @@ -425,12 +498,15 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.selectedProject.value!['id'].toString().isEmpty) { return _showError("Please select a project"); } - if (controller.selectedCategory.value == null) + if (controller.selectedCategory.value == null) { return _showError("Please select a category"); - if (controller.selectedPayee.value.isEmpty) + } + if (controller.selectedPayee.value.isEmpty) { return _showError("Please select a payee"); - if (controller.selectedCurrency.value == null) + } + if (controller.selectedCurrency.value == null) { return _showError("Please select currency"); + } return true; } diff --git a/lib/view/finance/payment_request_detail_screen.dart b/lib/view/finance/payment_request_detail_screen.dart index 8945252..f588173 100644 --- a/lib/view/finance/payment_request_detail_screen.dart +++ b/lib/view/finance/payment_request_detail_screen.dart @@ -20,6 +20,7 @@ import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/finance/payment_request_rembursement_bottom_sheet.dart'; import 'package:marco/model/finance/make_expense_bottom_sheet.dart'; +import 'package:marco/model/finance/add_payment_request_bottom_sheet.dart'; class PaymentRequestDetailScreen extends StatefulWidget { final String paymentRequestId; @@ -53,17 +54,7 @@ class _PaymentRequestDetailScreenState extends State final isCreatedByCurrentUser = employeeInfo?.id == request.createdBy.id; final hasDraftNextStatus = request.nextStatus.any((s) => s.id == draftStatusId); - - final result = isCreatedByCurrentUser && hasDraftNextStatus; - - // Debug log - print('🔐 Submit Permission Check:\n' - 'Logged-in employee: ${employeeInfo?.id}\n' - 'Created by: ${request.createdBy.id}\n' - 'Has Draft Next Status: $hasDraftNextStatus\n' - 'Can Submit: $result'); - - canSubmit.value = result; + canSubmit.value = isCreatedByCurrentUser && hasDraftNextStatus; } Future _loadEmployeeInfo() async { @@ -77,6 +68,38 @@ class _PaymentRequestDetailScreenState extends State return Color(int.parse(hex, radix: 16)); } + void _openEditPaymentRequestBottomSheet(request) { + showPaymentRequestBottomSheet( + isEdit: true, + existingData: { + "paymentRequestId": request.paymentRequestUID, + "title": request.title, + "projectId": request.project.id, + "projectName": request.project.name, + "expenseCategoryId": request.expenseCategory.id, + "expenseCategoryName": request.expenseCategory.name, + "amount": request.amount.toString(), + "currencyId": request.currency.id, + "currencySymbol": request.currency.symbol, + "payee": request.payee, + "description": request.description, + "isAdvancePayment": request.isAdvancePayment, + "dueDate": request.dueDate, + "attachments": request.attachments + .map((a) => { + "url": a.url, + "fileName": a.fileName, + "documentId": a.id, + "contentType": a.contentType, + }) + .toList(), + }, + onUpdated: () async { + await controller.fetchPaymentRequestDetail(); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -125,6 +148,7 @@ class _PaymentRequestDetailScreenState extends State _DetailsTable(request: request), const Divider(height: 30, thickness: 1.2), _Documents(documents: request.attachments), + MySpacing.height(24), ], ), ), @@ -135,15 +159,17 @@ class _PaymentRequestDetailScreenState extends State ); }), ), - bottomNavigationBar: Obx(() { + bottomNavigationBar: _buildBottomActionBar(), + + // ✅ Added Floating Action Button for Edit + floatingActionButton: Obx(() { + if (controller.isLoading.value) return const SizedBox.shrink(); + final request = controller.paymentRequest.value; - if (request == null || - controller.isLoading.value || - employeeInfo == null) { + if (controller.errorMessage.isNotEmpty || request == null) { return const SizedBox.shrink(); } - // Check permissions once if (!_checkedPermission) { _checkedPermission = true; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -151,101 +177,134 @@ class _PaymentRequestDetailScreenState extends State }); } - // Filter statuses - const reimbursementStatusId = '61578360-3a49-4c34-8604-7b35a3787b95'; - const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; + final canEdit = PaymentRequestPermissionHelper.canEditPaymentRequest( + employeeInfo, + request, + ); - final availableStatuses = request.nextStatus.where((status) { - if (status.id == draftStatusId) { - return employeeInfo?.id == request.createdBy.id; - } - return permissionController - .hasAnyPermission(status.permissionIds ?? []); - }).toList(); + if (!canEdit) return const SizedBox.shrink(); - // Normal status buttons - return SafeArea( - child: Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), - ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 10, - runSpacing: 10, - children: availableStatuses.map((status) { - final color = _parseColor(status.color); - - return ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 10), - backgroundColor: color, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () async { - // If status is reimbursement, show reimbursement bottom sheet - if (status.id == reimbursementStatusId) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical(top: Radius.circular(5)), - ), - builder: (ctx) => UpdatePaymentRequestWithReimbursement( - expenseId: request.paymentRequestUID, - statusId: status.id, - 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); - if (comment == null || comment.trim().isEmpty) return; - - final success = - await controller.updatePaymentRequestStatus( - statusId: status.id, - comment: comment.trim(), - ); - - showAppSnackbar( - title: success ? 'Success' : 'Error', - message: success - ? 'Status updated successfully' - : 'Failed to update status', - type: - success ? SnackbarType.success : SnackbarType.error, - ); - - if (success) await controller.fetchPaymentRequestDetail(); - } - }, - child: Text(status.displayName, - style: const TextStyle(color: Colors.white)), - ); - }).toList(), - ), + return FloatingActionButton.extended( + onPressed: () => _openEditPaymentRequestBottomSheet(request), + backgroundColor: contentTheme.primary, + icon: const Icon(Icons.edit), + label: MyText.bodyMedium( + "Edit Payment Request", + fontWeight: 600, + color: Colors.white, ), ); }), ); } + Widget _buildBottomActionBar() { + return Obx(() { + final request = controller.paymentRequest.value; + if (request == null || + controller.isLoading.value || + employeeInfo == null) { + return const SizedBox.shrink(); + } + + if (!_checkedPermission) { + _checkedPermission = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkPermissionToSubmit(request); + }); + } + + const reimbursementStatusId = '61578360-3a49-4c34-8604-7b35a3787b95'; + const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; + + final availableStatuses = request.nextStatus.where((status) { + if (status.id == draftStatusId) { + return employeeInfo?.id == request.createdBy.id; + } + return permissionController + .hasAnyPermission(status.permissionIds ?? []); + }).toList(); + + // Normal status buttons + return SafeArea( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: availableStatuses.map((status) { + final color = _parseColor(status.color); + + return ElevatedButton( + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + backgroundColor: color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async { + // If status is reimbursement, show reimbursement bottom sheet + if (status.id == reimbursementStatusId) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical(top: Radius.circular(5)), + ), + builder: (ctx) => UpdatePaymentRequestWithReimbursement( + expenseId: request.paymentRequestUID, + statusId: status.id, + 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); + if (comment == null || comment.trim().isEmpty) return; + + final success = await controller.updatePaymentRequestStatus( + statusId: status.id, + comment: comment.trim(), + ); + + showAppSnackbar( + title: success ? 'Success' : 'Error', + message: success + ? 'Status updated successfully' + : 'Failed to update status', + type: success ? SnackbarType.success : SnackbarType.error, + ); + + if (success) await controller.fetchPaymentRequestDetail(); + } + }, + child: Text(status.displayName, + style: const TextStyle(color: Colors.white)), + ); + }).toList(), + ), + ), + ); + }); + } + PreferredSizeWidget _buildAppBar() { return PreferredSize( preferredSize: const Size.fromHeight(72), @@ -306,6 +365,29 @@ class _PaymentRequestDetailScreenState extends State } } +class PaymentRequestPermissionHelper { + static bool canEditPaymentRequest( + EmployeeInfo? employee, PaymentRequestData request) { + return employee?.id == request.createdBy.id && + _isInAllowedEditStatus(request.expenseStatus.id); + } + + static bool canSubmitPaymentRequest( + EmployeeInfo? employee, PaymentRequestData request) { + return employee?.id == request.createdBy.id && + request.nextStatus.isNotEmpty; + } + + static bool _isInAllowedEditStatus(String statusId) { + const editableStatusIds = [ + "d1ee5eec-24b6-4364-8673-a8f859c60729", + "965eda62-7907-4963-b4a1-657fb0b2724b", + "297e0d8f-f668-41b5-bfea-e03b354251c8", + ]; + return editableStatusIds.contains(statusId); + } +} + class _Header extends StatelessWidget { final PaymentRequestData request; final Color Function(String) colorParser;