From fbfc54159c6136b00a5f04f7674f166a4260a9e2 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 8 Dec 2025 16:54:03 +0530 Subject: [PATCH] optimized code --- .../expense/expense_detail_controller.dart | 57 ++- lib/view/expense/expense_detail_screen.dart | 462 ++++++++---------- 2 files changed, 266 insertions(+), 253 deletions(-) diff --git a/lib/controller/expense/expense_detail_controller.dart b/lib/controller/expense/expense_detail_controller.dart index 9ba8376..85ac42a 100644 --- a/lib/controller/expense/expense_detail_controller.dart +++ b/lib/controller/expense/expense_detail_controller.dart @@ -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/employees/employee_model.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 { final Rx expense = Rx(null); @@ -16,6 +19,22 @@ class ExpenseDetailController extends GetxController { bool _isInitialized = false; final employeeSearchController = TextEditingController(); 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 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 Future _apiCallWrapper( Future Function() apiCall, String operationName) async { @@ -63,6 +112,8 @@ class ExpenseDetailController extends GetxController { try { expense.value = ExpenseDetailModel.fromJson(result); logSafe("Expense details loaded successfully: ${expense.value?.id}"); + // Call permission check after data is loaded + checkPermissionToSubmit(); } catch (e) { errorMessage.value = 'Failed to parse expense details: $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 parsePermissionIds(dynamic permissionData) { if (permissionData == null) return []; if (permissionData is List) { @@ -131,8 +180,6 @@ class ExpenseDetailController extends GetxController { allEmployees.clear(); 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 @@ -191,4 +238,4 @@ class ExpenseDetailController extends GetxController { return false; } } -} +} \ No newline at end of file diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index 3c459c6..2c15bc4 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -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/reimbursement_bottom_sheet.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: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/expense/expense_detail_helpers.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/services/storage/local_storage.dart'; import 'package:on_field_work/model/employees/employee_info.dart'; import 'package:timeline_tile/timeline_tile.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; @@ -37,15 +35,14 @@ class _ExpenseDetailScreenState extends State final projectController = Get.find(); final permissionController = Get.put(PermissionController()); - EmployeeInfo? employeeInfo; - final RxBool canSubmit = false.obs; - bool _checkedPermission = false; + // Removed local employeeInfo, canSubmit, and _checkedPermission + @override void initState() { super.initState(); controller = Get.put(ExpenseDetailController(), tag: widget.expenseId); - controller.init(widget.expenseId); - _loadEmployeeInfo(); + // EmployeeInfo loading and permission checking is now handled inside controller.init() + controller.init(widget.expenseId); } @override @@ -54,271 +51,239 @@ class _ExpenseDetailScreenState extends State super.dispose(); } - void _loadEmployeeInfo() async { - final info = await LocalStorage.getEmployeeInfo(); - employeeInfo = info; - } + // Removed _loadEmployeeInfo and _checkPermissionToSubmit - void _checkPermissionToSubmit(ExpenseDetailModel expense) { - const allowedNextStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; + @override + Widget build(BuildContext context) { + final Color appBarColor = contentTheme.primary; - final isCreatedByCurrentUser = employeeInfo?.id == expense.createdBy.id; - final nextStatusIds = expense.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: ${expense.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; - } - - @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), - ], + 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 - SafeArea( - child: Obx(() { - if (controller.isLoading.value) return buildLoadingSkeleton(); + // Main content + SafeArea( + child: Obx(() { + if (controller.isLoading.value) return buildLoadingSkeleton(); - final expense = controller.expense.value; - if (controller.errorMessage.isNotEmpty || expense == null) { - return Center(child: MyText.bodyMedium("No data to display.")); - } + final expense = controller.expense.value; + if (controller.errorMessage.isNotEmpty || expense == null) { + return Center(child: MyText.bodyMedium("No data to display.")); + } - WidgetsBinding.instance.addPostFrameCallback((_) { - _checkPermissionToSubmit(expense); - }); + // Permission logic moved to controller (no need for postFrameCallback here) - final statusColor = getExpenseStatusColor( - expense.status.name, - colorCode: expense.status.color, - ); - final formattedAmount = formatExpenseAmount(expense.amount); + final statusColor = getExpenseStatusColor( + expense.status.name, + colorCode: expense.status.color, + ); + final formattedAmount = formatExpenseAmount(expense.amount); - return MyRefreshIndicator( - onRefresh: () async { - await controller.fetchExpenseDetails(); - }, - child: SingleChildScrollView( - padding: EdgeInsets.fromLTRB( - 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom - ), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 520), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - elevation: 3, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 14, horizontal: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header & Status - _InvoiceHeader(expense: expense), - const Divider(height: 30, thickness: 1.2), + return MyRefreshIndicator( + onRefresh: () async { + await controller.fetchExpenseDetails(); + }, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom), + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 520), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + elevation: 3, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 14, horizontal: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header & Status + _InvoiceHeader(expense: expense), + const Divider(height: 30, thickness: 1.2), - // Activity Logs - InvoiceLogs(logs: expense.expenseLogs), - const Divider(height: 30, thickness: 1.2), + // Activity Logs + InvoiceLogs(logs: expense.expenseLogs), + const Divider(height: 30, thickness: 1.2), - // Amount & Summary - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium('Amount', fontWeight: 600), - const SizedBox(height: 4), - MyText.bodyLarge( - 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, - ), + // Amount & Summary + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + MyText.bodyMedium('Amount', + fontWeight: 600), + const SizedBox(height: 4), + MyText.bodyLarge( + formattedAmount, + fontWeight: 700, + color: statusColor, + ), + ], ), - ], - ), - const Divider(height: 30, thickness: 1.2), + 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 Divider(height: 30, thickness: 1.2), - // Parties - _InvoicePartiesTable(expense: expense), - const Divider(height: 30, thickness: 1.2), + // Parties + _InvoicePartiesTable(expense: expense), + const Divider(height: 30, thickness: 1.2), - // Expense Details - _InvoiceDetailsTable(expense: expense), - const Divider(height: 30, thickness: 1.2), + // Expense Details + _InvoiceDetailsTable(expense: expense), + const Divider(height: 30, thickness: 1.2), - // Documents - _InvoiceDocuments(documents: expense.documents), - const Divider(height: 30, thickness: 1.2), + // Documents + _InvoiceDocuments(documents: expense.documents), + const Divider(height: 30, thickness: 1.2), - // Totals - _InvoiceTotals( - expense: expense, - formattedAmount: formattedAmount, - statusColor: statusColor, - ), - ], + // Totals + _InvoiceTotals( + expense: expense, + formattedAmount: formattedAmount, + 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, - spacing: 10, - runSpacing: 10, - children: expense.nextStatus.where((next) { - const submitStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; + ], + ), + floatingActionButton: Obx(() { + final expense = controller.expense.value; + if (controller.errorMessage.isNotEmpty || expense == null) { + return const SizedBox.shrink(); + } - final rawPermissions = next.permissionIds; - final parsedPermissions = - controller.parsePermissionIds(rawPermissions); + // Removed _checkedPermission and its logic - final isSubmitStatus = next.id == submitStatusId; - final isCreatedByCurrentUser = - employeeInfo?.id == expense.createdBy.id; + if (!ExpensePermissionHelper.canEditExpense( + controller.employeeInfo, // Use controller's employeeInfo + expense)) { + return const SizedBox.shrink(); + } - if (isSubmitStatus) return isCreatedByCurrentUser; - return permissionController.hasAnyPermission(parsedPermissions); - }).map((next) { - return _statusButton(context, controller, expense, next); - }).toList(), + 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, + 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, ExpenseDetailModel expense, dynamic next) { @@ -346,7 +311,8 @@ Widget build(BuildContext context) { context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(5))), + borderRadius: + BorderRadius.vertical(top: Radius.circular(5))), builder: (context) => ReimbursementBottomSheet( expenseId: expense.id, statusId: next.id, @@ -819,4 +785,4 @@ class _InvoiceTotals extends StatelessWidget { ], ); } -} +} \ No newline at end of file