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;
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<bool> updateExpenseStatus(String statusId) async {
Future<bool> 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;
}
}
}
}

View File

@ -325,7 +325,6 @@ class ApiService {
}
/// Update Expense Status API
/// Update Expense Status API (supports optional reimbursement fields)
static Future<bool> updateExpenseStatusApi({
required String expenseId,
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: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(),
},
),
],
);