optimized code

This commit is contained in:
Vaibhav Surve 2025-12-08 16:54:03 +05:30
parent 7ce0a8555a
commit fbfc54159c
2 changed files with 266 additions and 253 deletions

View File

@ -4,6 +4,9 @@ import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:on_field_work/model/expense/expense_detail_model.dart'; import 'package:on_field_work/model/expense/expense_detail_model.dart';
import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:on_field_work/model/employees/employee_info.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
class ExpenseDetailController extends GetxController { class ExpenseDetailController extends GetxController {
final Rx<ExpenseDetailModel?> expense = Rx<ExpenseDetailModel?>(null); final Rx<ExpenseDetailModel?> expense = Rx<ExpenseDetailModel?>(null);
@ -16,6 +19,22 @@ class ExpenseDetailController extends GetxController {
bool _isInitialized = false; bool _isInitialized = false;
final employeeSearchController = TextEditingController(); final employeeSearchController = TextEditingController();
final isSearchingEmployees = false.obs; final isSearchingEmployees = false.obs;
// NEW: Holds the logged-in user info for permission checks
EmployeeInfo? employeeInfo;
final RxBool canSubmit = false.obs;
@override
void onInit() {
super.onInit();
_loadEmployeeInfo(); // Load employee info on init
}
void _loadEmployeeInfo() async {
final info = await LocalStorage.getEmployeeInfo();
employeeInfo = info;
}
/// 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) {
@ -31,6 +50,36 @@ class ExpenseDetailController extends GetxController {
]); ]);
} }
/// NEW: Logic to check if the current user can submit the expense
void checkPermissionToSubmit() {
final expenseData = expense.value;
if (employeeInfo == null || expenseData == null) {
canSubmit.value = false;
return;
}
// Status ID for 'Submit' (Hardcoded ID from the original screen logic)
const allowedNextStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7';
final isCreatedByCurrentUser = employeeInfo?.id == expenseData.createdBy.id;
final nextStatusIds = expenseData.nextStatus.map((e) => e.id).toList();
final hasRequiredNextStatus = nextStatusIds.contains(allowedNextStatusId);
final result = isCreatedByCurrentUser && hasRequiredNextStatus;
logSafe(
'🐛 Checking submit permission:\n'
'🐛 - Logged-in employee ID: ${employeeInfo?.id}\n'
'🐛 - Expense created by ID: ${expenseData.createdBy.id}\n'
'🐛 - Next Status IDs: $nextStatusIds\n'
'🐛 - Has Required Next Status ID ($allowedNextStatusId): $hasRequiredNextStatus\n'
'🐛 - Final Permission Result: $result',
level: LogLevel.debug,
);
canSubmit.value = result;
}
/// Generic method to handle API calls with loading and error states /// Generic method to handle API calls with loading and error states
Future<T?> _apiCallWrapper<T>( Future<T?> _apiCallWrapper<T>(
Future<T?> Function() apiCall, String operationName) async { Future<T?> Function() apiCall, String operationName) async {
@ -63,6 +112,8 @@ class ExpenseDetailController extends GetxController {
try { try {
expense.value = ExpenseDetailModel.fromJson(result); expense.value = ExpenseDetailModel.fromJson(result);
logSafe("Expense details loaded successfully: ${expense.value?.id}"); logSafe("Expense details loaded successfully: ${expense.value?.id}");
// Call permission check after data is loaded
checkPermissionToSubmit();
} catch (e) { } catch (e) {
errorMessage.value = 'Failed to parse expense details: $e'; errorMessage.value = 'Failed to parse expense details: $e';
logSafe("Parse error in fetchExpenseDetails: $e", logSafe("Parse error in fetchExpenseDetails: $e",
@ -75,8 +126,6 @@ class ExpenseDetailController extends GetxController {
} }
} }
// This method seems like a utility and might be better placed in a helper or utility class
// if it's used across multiple controllers. Keeping it here for now as per original code.
List<String> parsePermissionIds(dynamic permissionData) { List<String> parsePermissionIds(dynamic permissionData) {
if (permissionData == null) return []; if (permissionData == null) return [];
if (permissionData is List) { if (permissionData is List) {
@ -131,8 +180,6 @@ class ExpenseDetailController extends GetxController {
allEmployees.clear(); allEmployees.clear();
logSafe("No employees found.", level: LogLevel.warning); logSafe("No employees found.", level: LogLevel.warning);
} }
// `update()` is typically not needed for RxList directly unless you have specific GetBuilder/Obx usage that requires it
// If you are using Obx widgets, `allEmployees.assignAll` will automatically trigger a rebuild.
} }
/// Update expense with reimbursement info and status /// Update expense with reimbursement info and status
@ -191,4 +238,4 @@ class ExpenseDetailController extends GetxController {
return false; return false;
} }
} }
} }

View File

@ -11,14 +11,12 @@ import 'package:on_field_work/model/expense/comment_bottom_sheet.dart';
import 'package:on_field_work/model/expense/expense_detail_model.dart'; import 'package:on_field_work/model/expense/expense_detail_model.dart';
import 'package:on_field_work/model/expense/reimbursement_bottom_sheet.dart'; import 'package:on_field_work/model/expense/reimbursement_bottom_sheet.dart';
import 'package:on_field_work/controller/expense/add_expense_controller.dart'; import 'package:on_field_work/controller/expense/add_expense_controller.dart';
import 'package:on_field_work/helpers/services/app_logger.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart';
import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; import 'package:on_field_work/helpers/widgets/custom_app_bar.dart';
import 'package:on_field_work/helpers/widgets/expense/expense_detail_helpers.dart'; import 'package:on_field_work/helpers/widgets/expense/expense_detail_helpers.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/services/storage/local_storage.dart';
import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:on_field_work/model/employees/employee_info.dart';
import 'package:timeline_tile/timeline_tile.dart'; import 'package:timeline_tile/timeline_tile.dart';
import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart';
@ -37,15 +35,14 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
final projectController = Get.find<ProjectController>(); final projectController = Get.find<ProjectController>();
final permissionController = Get.put(PermissionController()); final permissionController = Get.put(PermissionController());
EmployeeInfo? employeeInfo; // Removed local employeeInfo, canSubmit, and _checkedPermission
final RxBool canSubmit = false.obs;
bool _checkedPermission = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
controller = Get.put(ExpenseDetailController(), tag: widget.expenseId); controller = Get.put(ExpenseDetailController(), tag: widget.expenseId);
controller.init(widget.expenseId); // EmployeeInfo loading and permission checking is now handled inside controller.init()
_loadEmployeeInfo(); controller.init(widget.expenseId);
} }
@override @override
@ -54,271 +51,239 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
super.dispose(); super.dispose();
} }
void _loadEmployeeInfo() async { // Removed _loadEmployeeInfo and _checkPermissionToSubmit
final info = await LocalStorage.getEmployeeInfo();
employeeInfo = info;
}
void _checkPermissionToSubmit(ExpenseDetailModel expense) { @override
const allowedNextStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; Widget build(BuildContext context) {
final Color appBarColor = contentTheme.primary;
final isCreatedByCurrentUser = employeeInfo?.id == expense.createdBy.id; return Scaffold(
final nextStatusIds = expense.nextStatus.map((e) => e.id).toList(); backgroundColor: const Color(0xFFF7F7F7),
final hasRequiredNextStatus = nextStatusIds.contains(allowedNextStatusId); appBar: CustomAppBar(
title: "Expense Details",
final result = isCreatedByCurrentUser && hasRequiredNextStatus; backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'),
logSafe( ),
'🐛 Checking submit permission:\n' body: Stack(
'🐛 - Logged-in employee ID: ${employeeInfo?.id}\n' children: [
'🐛 - Expense created by ID: ${expense.createdBy.id}\n' // Gradient behind content
'🐛 - Next Status IDs: $nextStatusIds\n' Container(
'🐛 - Has Required Next Status ID ($allowedNextStatusId): $hasRequiredNextStatus\n' height: kToolbarHeight + MediaQuery.of(context).padding.top,
'🐛 - Final Permission Result: $result', decoration: BoxDecoration(
level: LogLevel.debug, gradient: LinearGradient(
); begin: Alignment.topCenter,
end: Alignment.bottomCenter,
canSubmit.value = result; colors: [
} appBarColor,
appBarColor.withOpacity(0.0),
@override ],
Widget build(BuildContext context) { ),
final Color appBarColor = contentTheme.primary;
return Scaffold(
backgroundColor: const Color(0xFFF7F7F7),
appBar: CustomAppBar(
title: "Expense Details",
backgroundColor: appBarColor,
onBackPressed: () => Get.toNamed('/dashboard/expense-main-page'),
),
body: Stack(
children: [
// Gradient behind content
Container(
height: kToolbarHeight + MediaQuery.of(context).padding.top,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
appBarColor,
appBarColor.withOpacity(0.0),
],
), ),
), ),
),
// Main content // Main content
SafeArea( SafeArea(
child: Obx(() { child: Obx(() {
if (controller.isLoading.value) return buildLoadingSkeleton(); if (controller.isLoading.value) return buildLoadingSkeleton();
final expense = controller.expense.value; final expense = controller.expense.value;
if (controller.errorMessage.isNotEmpty || expense == null) { if (controller.errorMessage.isNotEmpty || expense == null) {
return Center(child: MyText.bodyMedium("No data to display.")); return Center(child: MyText.bodyMedium("No data to display."));
} }
WidgetsBinding.instance.addPostFrameCallback((_) { // Permission logic moved to controller (no need for postFrameCallback here)
_checkPermissionToSubmit(expense);
});
final statusColor = getExpenseStatusColor( final statusColor = getExpenseStatusColor(
expense.status.name, expense.status.name,
colorCode: expense.status.color, colorCode: expense.status.color,
); );
final formattedAmount = formatExpenseAmount(expense.amount); final formattedAmount = formatExpenseAmount(expense.amount);
return MyRefreshIndicator( return MyRefreshIndicator(
onRefresh: () async { onRefresh: () async {
await controller.fetchExpenseDetails(); await controller.fetchExpenseDetails();
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
12, 12, 12, 30 + MediaQuery.of(context).padding.bottom 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom),
), child: Center(
child: Center( child: Container(
child: Container( constraints: const BoxConstraints(maxWidth: 520),
constraints: const BoxConstraints(maxWidth: 520), child: Card(
child: Card( shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5)),
borderRadius: BorderRadius.circular(5)), elevation: 3,
elevation: 3, child: Padding(
child: Padding( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( vertical: 14, horizontal: 14),
vertical: 14, horizontal: 14), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ // Header & Status
// Header & Status _InvoiceHeader(expense: expense),
_InvoiceHeader(expense: expense), const Divider(height: 30, thickness: 1.2),
const Divider(height: 30, thickness: 1.2),
// Activity Logs // Activity Logs
InvoiceLogs(logs: expense.expenseLogs), InvoiceLogs(logs: expense.expenseLogs),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// Amount & Summary // Amount & Summary
Row( Row(
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
children: [ CrossAxisAlignment.start,
MyText.bodyMedium('Amount', fontWeight: 600), children: [
const SizedBox(height: 4), MyText.bodyMedium('Amount',
MyText.bodyLarge( fontWeight: 600),
formattedAmount, const SizedBox(height: 4),
fontWeight: 700, MyText.bodyLarge(
color: statusColor, formattedAmount,
), fontWeight: 700,
], color: statusColor,
), ),
const Spacer(), ],
if (expense.preApproved)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.15),
borderRadius: BorderRadius.circular(5),
),
child: MyText.bodySmall(
'Pre-Approved',
fontWeight: 600,
color: Colors.green,
),
), ),
], const Spacer(),
), if (expense.preApproved)
const Divider(height: 30, thickness: 1.2), Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.15),
borderRadius: BorderRadius.circular(5),
),
child: MyText.bodySmall(
'Pre-Approved',
fontWeight: 600,
color: Colors.green,
),
),
],
),
const Divider(height: 30, thickness: 1.2),
// Parties // Parties
_InvoicePartiesTable(expense: expense), _InvoicePartiesTable(expense: expense),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// Expense Details // Expense Details
_InvoiceDetailsTable(expense: expense), _InvoiceDetailsTable(expense: expense),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// Documents // Documents
_InvoiceDocuments(documents: expense.documents), _InvoiceDocuments(documents: expense.documents),
const Divider(height: 30, thickness: 1.2), const Divider(height: 30, thickness: 1.2),
// Totals // Totals
_InvoiceTotals( _InvoiceTotals(
expense: expense, expense: expense,
formattedAmount: formattedAmount, formattedAmount: formattedAmount,
statusColor: statusColor, statusColor: statusColor,
), ),
], ],
),
), ),
), ),
), ),
), ),
), ),
), );
); }),
}),
),
],
),
floatingActionButton: Obx(() {
if (controller.isLoading.value) return buildLoadingSkeleton();
final expense = controller.expense.value;
if (controller.errorMessage.isNotEmpty || expense == null) {
return const SizedBox.shrink();
}
if (!_checkedPermission) {
_checkedPermission = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkPermissionToSubmit(expense);
});
}
if (!ExpensePermissionHelper.canEditExpense(employeeInfo, expense)) {
return const SizedBox.shrink();
}
return FloatingActionButton.extended(
onPressed: () async {
final editData = {
'id': expense.id,
'projectName': expense.project.name,
'amount': expense.amount,
'supplerName': expense.supplerName,
'description': expense.description,
'transactionId': expense.transactionId,
'location': expense.location,
'transactionDate': expense.transactionDate,
'noOfPersons': expense.noOfPersons,
'expensesTypeId': expense.expensesType.id,
'paymentModeId': expense.paymentMode.id,
'paidById': expense.paidBy.id,
'paidByFirstName': expense.paidBy.firstName,
'paidByLastName': expense.paidBy.lastName,
'attachments': expense.documents
.map((doc) => {
'url': doc.preSignedUrl,
'fileName': doc.fileName,
'documentId': doc.documentId,
'contentType': doc.contentType,
})
.toList(),
};
final addCtrl = Get.put(AddExpenseController());
await addCtrl.loadMasterData();
addCtrl.populateFieldsForEdit(editData);
await showAddExpenseBottomSheet(isEdit: true);
await controller.fetchExpenseDetails();
},
backgroundColor: contentTheme.primary,
icon: const Icon(Icons.edit),
label: MyText.bodyMedium("Edit Expense",
fontWeight: 600, color: Colors.white),
);
}),
bottomNavigationBar: Obx(() {
final expense = controller.expense.value;
if (expense == null) return const SizedBox();
return SafeArea(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Color(0x11000000))),
), ),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), ],
child: Wrap( ),
alignment: WrapAlignment.center, floatingActionButton: Obx(() {
spacing: 10, final expense = controller.expense.value;
runSpacing: 10, if (controller.errorMessage.isNotEmpty || expense == null) {
children: expense.nextStatus.where((next) { return const SizedBox.shrink();
const submitStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; }
final rawPermissions = next.permissionIds; // Removed _checkedPermission and its logic
final parsedPermissions =
controller.parsePermissionIds(rawPermissions);
final isSubmitStatus = next.id == submitStatusId; if (!ExpensePermissionHelper.canEditExpense(
final isCreatedByCurrentUser = controller.employeeInfo, // Use controller's employeeInfo
employeeInfo?.id == expense.createdBy.id; expense)) {
return const SizedBox.shrink();
}
if (isSubmitStatus) return isCreatedByCurrentUser; return FloatingActionButton.extended(
return permissionController.hasAnyPermission(parsedPermissions); onPressed: () async {
}).map((next) { final editData = {
return _statusButton(context, controller, expense, next); 'id': expense.id,
}).toList(), 'projectName': expense.project.name,
'amount': expense.amount,
'supplerName': expense.supplerName,
'description': expense.description,
'transactionId': expense.transactionId,
'location': expense.location,
'transactionDate': expense.transactionDate,
'noOfPersons': expense.noOfPersons,
'expensesTypeId': expense.expensesType.id,
'paymentModeId': expense.paymentMode.id,
'paidById': expense.paidBy.id,
'paidByFirstName': expense.paidBy.firstName,
'paidByLastName': expense.paidBy.lastName,
'attachments': expense.documents
.map((doc) => {
'url': doc.preSignedUrl,
'fileName': doc.fileName,
'documentId': doc.documentId,
'contentType': doc.contentType,
})
.toList(),
};
final addCtrl = Get.put(AddExpenseController());
await addCtrl.loadMasterData();
addCtrl.populateFieldsForEdit(editData);
await showAddExpenseBottomSheet(isEdit: true);
await controller.fetchExpenseDetails();
},
backgroundColor: contentTheme.primary,
icon: const Icon(Icons.edit),
label: MyText.bodyMedium("Edit Expense",
fontWeight: 600, color: Colors.white),
);
}),
bottomNavigationBar: Obx(() {
final expense = controller.expense.value;
if (expense == null) return const SizedBox();
return SafeArea(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Color(0x11000000))),
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runSpacing: 10,
children: expense.nextStatus.where((next) {
const submitStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7';
final rawPermissions = next.permissionIds;
final parsedPermissions =
controller.parsePermissionIds(rawPermissions);
final isSubmitStatus = next.id == submitStatusId;
final isCreatedByCurrentUser =
controller.employeeInfo?.id == expense.createdBy.id; // Use controller's employeeInfo
if (isSubmitStatus) return isCreatedByCurrentUser;
return permissionController.hasAnyPermission(parsedPermissions);
}).map((next) {
return _statusButton(context, controller, expense, next);
}).toList(),
),
), ),
), );
); }),
}), );
); }
}
Widget _statusButton(BuildContext context, ExpenseDetailController controller, Widget _statusButton(BuildContext context, ExpenseDetailController controller,
ExpenseDetailModel expense, dynamic next) { ExpenseDetailModel expense, dynamic next) {
@ -346,7 +311,8 @@ Widget build(BuildContext context) {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(5))), borderRadius:
BorderRadius.vertical(top: Radius.circular(5))),
builder: (context) => ReimbursementBottomSheet( builder: (context) => ReimbursementBottomSheet(
expenseId: expense.id, expenseId: expense.id,
statusId: next.id, statusId: next.id,
@ -819,4 +785,4 @@ class _InvoiceTotals extends StatelessWidget {
], ],
); );
} }
} }