refactor: Enhance ExpenseDetailController and ExpenseDetailScreen to support optional comments during expense status updates and improve code readability
This commit is contained in:
parent
f245f9accf
commit
7dbc9138c6
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
67
lib/model/expense/comment_bottom_sheet.dart
Normal file
67
lib/model/expense/comment_bottom_sheet.dart
Normal 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);
|
@ -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(),
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user