refactor: Enhance ExpenseDetailController and ExpenseDetailScreen to support optional comments during expense status updates and improve code readability

This commit is contained in:
Vaibhav Surve 2025-08-05 10:09:34 +05:30
parent f245f9accf
commit 7dbc9138c6
4 changed files with 109 additions and 20 deletions

View File

@ -12,7 +12,8 @@ class ExpenseDetailController extends GetxController {
final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs; final RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
late String _expenseId; late String _expenseId;
bool _isInitialized = false; bool _isInitialized = false;
/// Call this once from the screen (NOT inside build) to initialize /// Call this once from the screen (NOT inside build) to initialize
void init(String expenseId) { void init(String expenseId) {
if (_isInitialized) return; if (_isInitialized) return;
@ -39,7 +40,8 @@ class ExpenseDetailController extends GetxController {
logSafe("$operationName completed successfully."); logSafe("$operationName completed successfully.");
return result; return result;
} catch (e, stack) { } 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("Exception in $operationName: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug); logSafe("StackTrace: $stack", level: LogLevel.debug);
return null; return null;
@ -133,7 +135,8 @@ class ExpenseDetailController extends GetxController {
"submit reimbursement", "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 await fetchExpenseDetails(); // Refresh details after successful update
return true; return true;
} else { } else {
@ -143,21 +146,22 @@ class ExpenseDetailController extends GetxController {
} }
/// Update status for this specific expense /// Update status for this specific expense
Future<bool> updateExpenseStatus(String statusId) async { Future<bool> updateExpenseStatus(String statusId, {String? comment}) async {
final success = await _apiCallWrapper( final success = await _apiCallWrapper(
() => ApiService.updateExpenseStatusApi( () => ApiService.updateExpenseStatusApi(
expenseId: _expenseId, expenseId: _expenseId,
statusId: statusId, statusId: statusId,
comment: comment,
), ),
"update expense status", "update expense status",
); );
if (success == true) { // Explicitly check for true as _apiCallWrapper returns T? if (success == true) {
await fetchExpenseDetails(); // Refresh details after successful update await fetchExpenseDetails();
return true; return true;
} else { } else {
errorMessage.value = "Failed to update expense status."; errorMessage.value = "Failed to update expense status.";
return false; return false;
} }
} }
} }

View File

@ -325,7 +325,6 @@ class ApiService {
} }
/// Update Expense Status API /// Update Expense Status API
/// Update Expense Status API (supports optional reimbursement fields)
static Future<bool> updateExpenseStatusApi({ static Future<bool> updateExpenseStatusApi({
required String expenseId, required String expenseId,
required String statusId, required String statusId,

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
Future<String?> showCommentBottomSheet(BuildContext context, String actionText) async {
final commentController = TextEditingController();
String? errorText;
return showModalBottomSheet<String>(
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);

View File

@ -13,6 +13,7 @@ import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:marco/model/expense/reimbursement_bottom_sheet.dart'; import 'package:marco/model/expense/reimbursement_bottom_sheet.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/expense/comment_bottom_sheet.dart';
class ExpenseDetailScreen extends StatelessWidget { class ExpenseDetailScreen extends StatelessWidget {
final String expenseId; final String expenseId;
@ -195,8 +196,11 @@ class ExpenseDetailScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(6)), borderRadius: BorderRadius.circular(6)),
), ),
onPressed: () async { onPressed: () async {
if (expense.status.id == const reimbursementId =
'f18c5cfd-7815-4341-8da2-2c2d65778e27') { 'f18c5cfd-7815-4341-8da2-2c2d65778e27';
if (expense.status.id == reimbursementId) {
// Open reimbursement flow
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -244,8 +248,15 @@ class ExpenseDetailScreen extends StatelessWidget {
), ),
); );
} else { } else {
final success = // New: Show comment sheet
await controller.updateExpenseStatus(next.id); final comment =
await showCommentBottomSheet(context, next.name);
if (comment == null) return;
final success = await controller.updateExpenseStatus(
next.id,
comment: comment,
);
if (success) { if (success) {
showAppSnackbar( showAppSnackbar(
@ -457,14 +468,20 @@ class _InvoiceDocuments extends StatelessWidget {
if (documents.isEmpty) { if (documents.isEmpty) {
return MyText.bodyMedium('No Supporting Documents', color: Colors.grey); return MyText.bodyMedium('No Supporting Documents', color: Colors.grey);
} }
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.bodySmall("Supporting Documents:", fontWeight: 600), MyText.bodySmall("Supporting Documents:", fontWeight: 600),
const SizedBox(height: 8), const SizedBox(height: 12),
Wrap( ListView.separated(
spacing: 10, shrinkWrap: true,
children: documents.map((doc) { physics: const NeverScrollableScrollPhysics(),
itemCount: documents.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final doc = documents[index];
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
final imageDocs = documents final imageDocs = documents
@ -505,7 +522,6 @@ class _InvoiceDocuments extends StatelessWidget {
color: Colors.grey.shade100, color: Colors.grey.shade100,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
doc.contentType.startsWith('image/') doc.contentType.startsWith('image/')
@ -515,14 +531,17 @@ class _InvoiceDocuments extends StatelessWidget {
color: Colors.grey[600], color: Colors.grey[600],
), ),
const SizedBox(width: 7), const SizedBox(width: 7),
MyText.labelSmall( Expanded(
doc.fileName, child: MyText.labelSmall(
doc.fileName,
overflow: TextOverflow.ellipsis,
),
), ),
], ],
), ),
), ),
); );
}).toList(), },
), ),
], ],
); );