diff --git a/lib/controller/expense/expense_detail_controller.dart b/lib/controller/expense/expense_detail_controller.dart index 23d3d4e..4bc8561 100644 --- a/lib/controller/expense/expense_detail_controller.dart +++ b/lib/controller/expense/expense_detail_controller.dart @@ -12,7 +12,8 @@ class ExpenseDetailController extends GetxController { final RxList allEmployees = [].obs; late String _expenseId; - bool _isInitialized = false; + bool _isInitialized = false; + /// Call this once from the screen (NOT inside build) to initialize void init(String expenseId) { if (_isInitialized) return; @@ -39,7 +40,8 @@ class ExpenseDetailController extends GetxController { logSafe("$operationName completed successfully."); return result; } catch (e, stack) { - errorMessage.value = 'An unexpected error occurred during $operationName.'; + errorMessage.value = + 'An unexpected error occurred during $operationName.'; logSafe("Exception in $operationName: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); return null; @@ -133,7 +135,8 @@ class ExpenseDetailController extends GetxController { "submit reimbursement", ); - if (success == true) { // Explicitly check for true as _apiCallWrapper returns T? + if (success == true) { + // Explicitly check for true as _apiCallWrapper returns T? await fetchExpenseDetails(); // Refresh details after successful update return true; } else { @@ -143,21 +146,22 @@ class ExpenseDetailController extends GetxController { } /// Update status for this specific expense - Future updateExpenseStatus(String statusId) async { + Future updateExpenseStatus(String statusId, {String? comment}) async { final success = await _apiCallWrapper( () => ApiService.updateExpenseStatusApi( expenseId: _expenseId, statusId: statusId, + comment: comment, ), "update expense status", ); - if (success == true) { // Explicitly check for true as _apiCallWrapper returns T? - await fetchExpenseDetails(); // Refresh details after successful update + if (success == true) { + await fetchExpenseDetails(); return true; } else { errorMessage.value = "Failed to update expense status."; return false; } } -} \ No newline at end of file +} diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 2107512..1ab3685 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -325,7 +325,6 @@ class ApiService { } /// Update Expense Status API - /// Update Expense Status API (supports optional reimbursement fields) static Future updateExpenseStatusApi({ required String expenseId, required String statusId, diff --git a/lib/model/expense/comment_bottom_sheet.dart b/lib/model/expense/comment_bottom_sheet.dart new file mode 100644 index 0000000..447a629 --- /dev/null +++ b/lib/model/expense/comment_bottom_sheet.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; + +Future showCommentBottomSheet(BuildContext context, String actionText) async { + final commentController = TextEditingController(); + String? errorText; + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return StatefulBuilder( + builder: (context, setModalState) { + void submit() { + final comment = commentController.text.trim(); + if (comment.isEmpty) { + setModalState(() => errorText = 'Comment cannot be empty.'); + return; + } + Navigator.of(context).pop(comment); + } + + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: BaseBottomSheet( + title: 'Add Comment for ${_capitalizeFirstLetter(actionText)}', + onCancel: () => Navigator.of(context).pop(), + onSubmit: submit, + isSubmitting: false, + submitText: 'Submit', + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: commentController, + maxLines: 4, + decoration: InputDecoration( + hintText: 'Type your comment here...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade100, + errorText: errorText, + ), + onChanged: (_) { + if (errorText != null) { + setModalState(() => errorText = null); + } + }, + ), + ], + ), + ), + ); + }, + ); + }, + ); +} + +String _capitalizeFirstLetter(String text) => + text.isEmpty ? text : text[0].toUpperCase() + text.substring(1); diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index 9af777e..102bb97 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -13,6 +13,7 @@ import 'package:marco/helpers/widgets/image_viewer_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:marco/model/expense/reimbursement_bottom_sheet.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/model/expense/comment_bottom_sheet.dart'; class ExpenseDetailScreen extends StatelessWidget { final String expenseId; @@ -195,8 +196,11 @@ class ExpenseDetailScreen extends StatelessWidget { borderRadius: BorderRadius.circular(6)), ), onPressed: () async { - if (expense.status.id == - 'f18c5cfd-7815-4341-8da2-2c2d65778e27') { + const reimbursementId = + 'f18c5cfd-7815-4341-8da2-2c2d65778e27'; + + if (expense.status.id == reimbursementId) { + // Open reimbursement flow showModalBottomSheet( context: context, isScrollControlled: true, @@ -244,8 +248,15 @@ class ExpenseDetailScreen extends StatelessWidget { ), ); } else { - final success = - await controller.updateExpenseStatus(next.id); + // ✨ New: Show comment sheet + final comment = + await showCommentBottomSheet(context, next.name); + if (comment == null) return; + + final success = await controller.updateExpenseStatus( + next.id, + comment: comment, + ); if (success) { showAppSnackbar( @@ -457,14 +468,20 @@ class _InvoiceDocuments extends StatelessWidget { if (documents.isEmpty) { return MyText.bodyMedium('No Supporting Documents', color: Colors.grey); } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall("Supporting Documents:", fontWeight: 600), - const SizedBox(height: 8), - Wrap( - spacing: 10, - children: documents.map((doc) { + const SizedBox(height: 12), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: documents.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final doc = documents[index]; + return GestureDetector( onTap: () async { final imageDocs = documents @@ -505,7 +522,6 @@ class _InvoiceDocuments extends StatelessWidget { color: Colors.grey.shade100, ), child: Row( - mainAxisSize: MainAxisSize.min, children: [ Icon( doc.contentType.startsWith('image/') @@ -515,14 +531,17 @@ class _InvoiceDocuments extends StatelessWidget { color: Colors.grey[600], ), const SizedBox(width: 7), - MyText.labelSmall( - doc.fileName, + Expanded( + child: MyText.labelSmall( + doc.fileName, + overflow: TextOverflow.ellipsis, + ), ), ], ), ), ); - }).toList(), + }, ), ], );