diff --git a/lib/controller/expense/expense_detail_controller.dart b/lib/controller/expense/expense_detail_controller.dart index ab47eaf..8023c0a 100644 --- a/lib/controller/expense/expense_detail_controller.dart +++ b/lib/controller/expense/expense_detail_controller.dart @@ -11,35 +11,40 @@ class ExpenseDetailController extends GetxController { final Rx selectedReimbursedBy = Rx(null); final RxList allEmployees = [].obs; - @override - void onInit() { - super.onInit(); + bool _isInitialized = false; + late String _expenseId; + + /// Call this once from the screen (NOT inside build) to initialize + void init(String expenseId) { + if (_isInitialized) return; + + _isInitialized = true; + _expenseId = expenseId; + + fetchExpenseDetails(); fetchAllEmployees(); } - /// Fetch expense details by ID - Future fetchExpenseDetails(String expenseId) async { + /// Fetch expense details by stored ID + Future fetchExpenseDetails() async { isLoading.value = true; errorMessage.value = ''; try { - logSafe("Fetching expense details for ID: $expenseId"); + logSafe("Fetching expense details for ID: $_expenseId"); - final result = - await ApiService.getExpenseDetailsApi(expenseId: expenseId); + final result = await ApiService.getExpenseDetailsApi(expenseId: _expenseId); if (result != null) { try { expense.value = ExpenseDetailModel.fromJson(result); logSafe("Expense details loaded successfully: ${expense.value?.id}"); } catch (e) { errorMessage.value = 'Failed to parse expense details: $e'; - logSafe("Parse error in fetchExpenseDetails: $e", - level: LogLevel.error); + logSafe("Parse error in fetchExpenseDetails: $e", level: LogLevel.error); } } else { errorMessage.value = 'Failed to fetch expense details from server.'; - logSafe("fetchExpenseDetails failed: null response", - level: LogLevel.error); + logSafe("fetchExpenseDetails failed: null response", level: LogLevel.error); } } catch (e, stack) { errorMessage.value = 'An unexpected error occurred.'; @@ -59,8 +64,7 @@ class ExpenseDetailController extends GetxController { final response = await ApiService.getAllEmployees(); if (response != null && response.isNotEmpty) { allEmployees.assignAll(response.map((e) => EmployeeModel.fromJson(e))); - logSafe("All Employees fetched: ${allEmployees.length}", - level: LogLevel.info); + logSafe("All Employees fetched: ${allEmployees.length}", level: LogLevel.info); } else { allEmployees.clear(); logSafe("No employees found.", level: LogLevel.warning); @@ -76,22 +80,21 @@ class ExpenseDetailController extends GetxController { /// Update expense with reimbursement info and status Future updateExpenseStatusWithReimbursement({ - required String expenseId, required String comment, required String reimburseTransactionId, required String reimburseDate, required String reimburseById, - required String statusId, // ✅ dynamic + required String statusId, }) async { isLoading.value = true; errorMessage.value = ''; try { - logSafe("Submitting reimbursement for expense: $expenseId"); + logSafe("Submitting reimbursement for expense: $_expenseId"); final success = await ApiService.updateExpenseStatusApi( - expenseId: expenseId, - statusId: statusId, // ✅ now dynamic + expenseId: _expenseId, + statusId: statusId, comment: comment, reimburseTransactionId: reimburseTransactionId, reimburseDate: reimburseDate, @@ -100,7 +103,7 @@ class ExpenseDetailController extends GetxController { if (success) { logSafe("Reimbursement submitted successfully."); - await fetchExpenseDetails(expenseId); + await fetchExpenseDetails(); // refresh latest return true; } else { errorMessage.value = "Failed to submit reimbursement."; @@ -108,8 +111,7 @@ class ExpenseDetailController extends GetxController { } } catch (e, stack) { errorMessage.value = 'An unexpected error occurred.'; - logSafe("Exception in updateExpenseStatusWithReimbursement: $e", - level: LogLevel.error); + logSafe("Exception in updateExpenseStatusWithReimbursement: $e", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); return false; } finally { @@ -118,21 +120,21 @@ class ExpenseDetailController extends GetxController { } /// Update status for this specific expense - Future updateExpenseStatus(String expenseId, String statusId) async { + Future updateExpenseStatus(String statusId) async { isLoading.value = true; errorMessage.value = ''; try { - logSafe("Updating status for expense: $expenseId -> $statusId"); + logSafe("Updating status for expense: $_expenseId -> $statusId"); final success = await ApiService.updateExpenseStatusApi( - expenseId: expenseId, + expenseId: _expenseId, statusId: statusId, ); if (success) { logSafe("Expense status updated successfully."); - await fetchExpenseDetails(expenseId); + await fetchExpenseDetails(); // refresh return true; } else { errorMessage.value = "Failed to update expense status."; diff --git a/lib/helpers/utils/base_bottom_sheet.dart b/lib/helpers/utils/base_bottom_sheet.dart new file mode 100644 index 0000000..b7ed51c --- /dev/null +++ b/lib/helpers/utils/base_bottom_sheet.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class BaseBottomSheet extends StatelessWidget { + final String title; + final Widget child; + final VoidCallback onCancel; + final VoidCallback onSubmit; + final bool isSubmitting; + final String submitText; + final Color submitColor; + final IconData submitIcon; + + const BaseBottomSheet({ + super.key, + required this.title, + required this.child, + required this.onCancel, + required this.onSubmit, + this.isSubmitting = false, + this.submitText = 'Submit', + this.submitColor = Colors.indigo, + this.submitIcon = Icons.check_circle_outline, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final mediaQuery = MediaQuery.of(context); + + return SingleChildScrollView( + padding: mediaQuery.viewInsets, + child: Padding( + padding: const EdgeInsets.only( + top: 60), + child: Container( + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MySpacing.height(5), + Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(10), + ), + ), + MySpacing.height(12), + MyText.titleLarge(title, fontWeight: 700), + MySpacing.height(12), + child, + MySpacing.height(24), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: onCancel, + icon: const Icon(Icons.close, color: Colors.white), + label: MyText.bodyMedium( + "Cancel", + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric( + vertical: 8), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: isSubmitting ? null : onSubmit, + icon: Icon(submitIcon, color: Colors.white), + label: MyText.bodyMedium( + isSubmitting ? "Submitting..." : submitText, + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: submitColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric( + vertical: 8), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/helpers/utils/permission_constants.dart b/lib/helpers/utils/permission_constants.dart index 8160964..81ee465 100644 --- a/lib/helpers/utils/permission_constants.dart +++ b/lib/helpers/utils/permission_constants.dart @@ -3,9 +3,9 @@ class Permissions { static const String manageProject = "172fc9b6-755b-4f62-ab26-55c34a330614"; static const String viewProjects = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"; static const String manageEmployees = "a97d366a-c2bb-448d-be93-402bd2324566"; - static const String manageProjectInfra ="f2aee20a-b754-4537-8166-f9507b44585b"; + static const String manageProjectInfra = "f2aee20a-b754-4537-8166-f9507b44585b"; static const String viewProjectInfra = "c7b68e33-72f0-474f-bd96-77636427ecc8"; - static const String regularizeAttendance ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; + static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; static const String assignToProject = "fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"; static const String infrastructure = "9666de86-d7c7-4d3d-acaa-fcd6d6b81f3c"; static const String manageTask = "08752f33-3b29-4816-b76b-ea8a968ed3c5"; @@ -13,4 +13,13 @@ class Permissions { static const String assignReportTask = "6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"; static const String directoryAdmin = "4286a13b-bb40-4879-8c6d-18e9e393beda"; static const String directoryManager = "62668630-13ce-4f52-a0f0-db38af2230c5"; + + // Expense Permissions + static const String expenseViewSelf = "385be49f-8fde-440e-bdbc-3dffeb8dd116"; + static const String expenseViewAll = "01e06444-9ca7-4df4-b900-8c3fa051b92f"; + static const String expenseUpload = "0f57885d-bcb2-4711-ac95-d841ace6d5a7"; + static const String expenseReview = "1f4bda08-1873-449a-bb66-3e8222bd871b"; + static const String expenseApprove = "eaafdd76-8aac-45f9-a530-315589c6deca"; + static const String expenseProcess = "ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"; + static const String expenseManage = "bdee29a2-b73b-402d-8dd1-c4b1f81ccbc3"; } diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart index 6354ee3..6723c44 100644 --- a/lib/model/expense/add_expense_bottom_sheet.dart +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -1,11 +1,11 @@ import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/expense/add_expense_controller.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/model/expense/payment_types_model.dart'; import 'package:marco/model/expense/expense_type_model.dart'; +import 'package:marco/model/expense/payment_types_model.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; void showAddExpenseBottomSheet() { Get.bottomSheet(const _AddExpenseBottomSheet(), isScrollControlled: true); @@ -83,309 +83,228 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> { @override Widget build(BuildContext context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 60), - child: Material( - color: Colors.white, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - child: Obx(() { - return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _DragHandle(), - Center( - child: MyText.titleLarge("Add Expense", fontWeight: 700), - ), - const SizedBox(height: 20), - _buildSectionWithDropdown( - icon: Icons.work_outline, - title: "Project", - requiredField: true, - currentValue: controller.selectedProject.value.isEmpty - ? "Select Project" - : controller.selectedProject.value, - onTap: () => _showOptionList( - controller.globalProjects.toList(), - (p) => p, - (val) => controller.selectedProject.value = val), - ), - const SizedBox(height: 16), - _buildSectionWithDropdown( - icon: Icons.category_outlined, - title: "Expense Type", - requiredField: true, - currentValue: controller.selectedExpenseType.value?.name ?? - "Select Expense Type", - onTap: () => _showOptionList( - controller.expenseTypes.toList(), - (e) => e.name, - (val) => controller.selectedExpenseType.value = val, - ), - ), - if (controller - .selectedExpenseType.value?.noOfPersonsRequired == - true) - Padding( - padding: const EdgeInsets.only(top: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _SectionTitle( - icon: Icons.people_outline, - title: "No. of Persons", - requiredField: true, - ), - const SizedBox(height: 6), - _CustomTextField( - controller: controller.noOfPersonsController, - hint: "Enter No. of Persons", - keyboardType: TextInputType.number, - ), - ], - ), - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.confirmation_number_outlined, - title: "GST No.", - ), - const SizedBox(height: 6), - _CustomTextField( - controller: controller.gstController, - hint: "Enter GST No.", - ), - const SizedBox(height: 16), - _buildSectionWithDropdown( - icon: Icons.payment, - title: "Payment Mode", - requiredField: true, - currentValue: controller.selectedPaymentMode.value?.name ?? - "Select Payment Mode", - onTap: () => _showOptionList( - controller.paymentModes.toList(), - (m) => m.name, - (val) => controller.selectedPaymentMode.value = val, - ), - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.person_outline, - title: "Paid By", - requiredField: true), - const SizedBox(height: 6), - GestureDetector( - onTap: _showEmployeeList, - child: _TileContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - controller.selectedPaidBy.value == null - ? "Select Paid By" - : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', - style: const TextStyle(fontSize: 14), - ), - const Icon(Icons.arrow_drop_down, size: 22), - ], - ), - ), - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.currency_rupee, - title: "Amount", - requiredField: true), - const SizedBox(height: 6), - _CustomTextField( - controller: controller.amountController, - hint: "Enter Amount", - keyboardType: TextInputType.number, - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.store_mall_directory_outlined, - title: "Supplier Name", - requiredField: true), - const SizedBox(height: 6), - _CustomTextField( - controller: controller.supplierController, - hint: "Enter Supplier Name", - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.confirmation_number_outlined, - title: "Transaction ID"), - const SizedBox(height: 6), - _CustomTextField( - controller: controller.transactionIdController, - hint: "Enter Transaction ID", - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.calendar_today, - title: "Transaction Date", - requiredField: true), - const SizedBox(height: 6), - GestureDetector( - onTap: () => controller.pickTransactionDate(context), - child: AbsorbPointer( - child: _CustomTextField( - controller: controller.transactionDateController, - hint: "Select Transaction Date", - ), - ), - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.location_on_outlined, title: "Location"), - const SizedBox(height: 6), - TextField( - controller: controller.locationController, - decoration: InputDecoration( - hintText: "Enter Location", - filled: true, - fillColor: Colors.grey.shade100, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8)), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 10), - suffixIcon: controller.isFetchingLocation.value - ? const Padding( - padding: EdgeInsets.all(12), - child: SizedBox( - width: 18, - height: 18, - child: - CircularProgressIndicator(strokeWidth: 2), - ), - ) - : IconButton( - icon: const Icon(Icons.my_location), - tooltip: "Use Current Location", - onPressed: controller.fetchCurrentLocation, - ), - ), - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.attach_file, - title: "Attachments", - requiredField: true), - const SizedBox(height: 6), - _AttachmentsSection( - attachments: controller.attachments, - onRemove: controller.removeAttachment, - onAdd: controller.pickAttachments, - ), - const SizedBox(height: 16), - _SectionTitle( - icon: Icons.description_outlined, - title: "Description", - requiredField: true), - const SizedBox(height: 6), - _CustomTextField( - controller: controller.descriptionController, - hint: "Enter Description", - maxLines: 3, - ), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: Get.back, - icon: const Icon(Icons.close, size: 18), - label: MyText.bodyMedium("Cancel", fontWeight: 600), - style: OutlinedButton.styleFrom( - minimumSize: const Size.fromHeight(48)), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Obx( - () { - final isLoading = controller.isSubmitting.value; - return ElevatedButton.icon( - onPressed: - isLoading ? null : controller.submitExpense, - icon: isLoading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, color: Colors.white), - ) - : const Icon(Icons.check, size: 18), - label: MyText.bodyMedium( - isLoading ? "Submitting..." : "Submit", - color: Colors.white, - fontWeight: 600, - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), - padding: - const EdgeInsets.symmetric(vertical: 14), - minimumSize: const Size.fromHeight(48), - ), - ); - }, - ), - ), - ], - ), - ], + return Obx(() { + return BaseBottomSheet( + title: "Add Expense", + isSubmitting: controller.isSubmitting.value, + onCancel: Get.back, + onSubmit: () { + if (!controller.isSubmitting.value) { + controller.submitExpense(); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDropdown( + icon: Icons.work_outline, + title: "Project", + requiredField: true, + value: controller.selectedProject.value.isEmpty + ? "Select Project" + : controller.selectedProject.value, + onTap: () => _showOptionList( + controller.globalProjects.toList(), + (p) => p, + (val) => controller.selectedProject.value = val, ), - ); - }), + ), + MySpacing.height(16), + _buildDropdown( + icon: Icons.category_outlined, + title: "Expense Type", + requiredField: true, + value: controller.selectedExpenseType.value?.name ?? + "Select Expense Type", + onTap: () => _showOptionList( + controller.expenseTypes.toList(), + (e) => e.name, + (val) => controller.selectedExpenseType.value = val, + ), + ), + if (controller.selectedExpenseType.value?.noOfPersonsRequired == + true) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SectionTitle( + icon: Icons.people_outline, + title: "No. of Persons", + requiredField: true, + ), + MySpacing.height(6), + _CustomTextField( + controller: controller.noOfPersonsController, + hint: "Enter No. of Persons", + keyboardType: TextInputType.number, + ), + ], + ), + ), + MySpacing.height(16), + _SectionTitle( + icon: Icons.confirmation_number_outlined, title: "GST No."), + MySpacing.height(6), + _CustomTextField( + controller: controller.gstController, hint: "Enter GST No."), + MySpacing.height(16), + _buildDropdown( + icon: Icons.payment, + title: "Payment Mode", + requiredField: true, + value: controller.selectedPaymentMode.value?.name ?? + "Select Payment Mode", + onTap: () => _showOptionList( + controller.paymentModes.toList(), + (p) => p.name, + (val) => controller.selectedPaymentMode.value = val, + ), + ), + MySpacing.height(16), + _SectionTitle( + icon: Icons.person_outline, + title: "Paid By", + requiredField: true), + MySpacing.height(6), + GestureDetector( + onTap: _showEmployeeList, + child: _TileContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + controller.selectedPaidBy.value == null + ? "Select Paid By" + : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', + style: const TextStyle(fontSize: 14), + ), + const Icon(Icons.arrow_drop_down, size: 22), + ], + ), + ), + ), + MySpacing.height(16), + _SectionTitle( + icon: Icons.currency_rupee, + title: "Amount", + requiredField: true), + MySpacing.height(6), + _CustomTextField( + controller: controller.amountController, + hint: "Enter Amount", + keyboardType: TextInputType.number, + ), + MySpacing.height(16), + _SectionTitle( + icon: Icons.store_mall_directory_outlined, + title: "Supplier Name", + requiredField: true, + ), + MySpacing.height(6), + _CustomTextField( + controller: controller.supplierController, + hint: "Enter Supplier Name"), + MySpacing.height(16), + _SectionTitle( + icon: Icons.confirmation_number_outlined, + title: "Transaction ID"), + MySpacing.height(6), + _CustomTextField( + controller: controller.transactionIdController, + hint: "Enter Transaction ID"), + MySpacing.height(16), + _SectionTitle( + icon: Icons.calendar_today, + title: "Transaction Date", + requiredField: true, + ), + MySpacing.height(6), + GestureDetector( + onTap: () => controller.pickTransactionDate(context), + child: AbsorbPointer( + child: _CustomTextField( + controller: controller.transactionDateController, + hint: "Select Transaction Date", + ), + ), + ), + MySpacing.height(16), + _SectionTitle(icon: Icons.location_on_outlined, title: "Location"), + MySpacing.height(6), + TextField( + controller: controller.locationController, + decoration: InputDecoration( + hintText: "Enter Location", + filled: true, + fillColor: Colors.grey.shade100, + border: + OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + suffixIcon: controller.isFetchingLocation.value + ? const Padding( + padding: EdgeInsets.all(12), + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : IconButton( + icon: const Icon(Icons.my_location), + tooltip: "Use Current Location", + onPressed: controller.fetchCurrentLocation, + ), + ), + ), + MySpacing.height(16), + _SectionTitle( + icon: Icons.attach_file, + title: "Attachments", + requiredField: true), + MySpacing.height(6), + _AttachmentsSection( + attachments: controller.attachments, + onRemove: controller.removeAttachment, + onAdd: controller.pickAttachments, + ), + MySpacing.height(16), + _SectionTitle( + icon: Icons.description_outlined, + title: "Description", + requiredField: true), + MySpacing.height(6), + _CustomTextField( + controller: controller.descriptionController, + hint: "Enter Description", + maxLines: 3, + ), + ], ), - ), - ); + ); + }); } - Widget _buildSectionWithDropdown({ + Widget _buildDropdown({ required IconData icon, required String title, required bool requiredField, - required String currentValue, + required String value, required VoidCallback onTap, - Widget? extraWidget, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SectionTitle(icon: icon, title: title, requiredField: requiredField), - const SizedBox(height: 6), - _DropdownTile(title: currentValue, onTap: onTap), - if (extraWidget != null) extraWidget, + MySpacing.height(6), + _DropdownTile(title: value, onTap: onTap), ], ); } } -class _DragHandle extends StatelessWidget { - const _DragHandle(); - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.grey.shade400, - borderRadius: BorderRadius.circular(2), - ), - ), - ); - } -} - class _SectionTitle extends StatelessWidget { final IconData icon; final String title; @@ -447,11 +366,23 @@ class _CustomTextField extends StatelessWidget { keyboardType: keyboardType, decoration: InputDecoration( hintText: hint, + hintStyle: TextStyle(fontSize: 14, color: Colors.grey[600]), filled: true, fillColor: Colors.grey.shade100, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.blueAccent, width: 1.5), + ), ), ); } diff --git a/lib/model/expense/reimbursement_bottom_sheet.dart b/lib/model/expense/reimbursement_bottom_sheet.dart index 016da67..b22b26d 100644 --- a/lib/model/expense/reimbursement_bottom_sheet.dart +++ b/lib/model/expense/reimbursement_bottom_sheet.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; + import 'package:marco/controller/expense/expense_detail_controller.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; class ReimbursementBottomSheet extends StatefulWidget { final String expenseId; @@ -30,8 +34,7 @@ class ReimbursementBottomSheet extends StatefulWidget { } class _ReimbursementBottomSheetState extends State { - final ExpenseDetailController controller = - Get.find(); + final ExpenseDetailController controller = Get.find(); final TextEditingController commentCtrl = TextEditingController(); final TextEditingController txnCtrl = TextEditingController(); @@ -49,14 +52,16 @@ class _ReimbursementBottomSheetState extends State { context: context, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), builder: (_) { return SizedBox( height: 300, child: Obx(() { final employees = controller.allEmployees; - if (employees.isEmpty) + if (employees.isEmpty) { return const Center(child: Text("No employees found")); + } return ListView.builder( itemCount: employees.length, itemBuilder: (_, index) { @@ -77,221 +82,128 @@ class _ReimbursementBottomSheetState extends State { ); } + InputDecoration _inputDecoration(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: MyTextStyle.bodySmall(xMuted: true), + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5), + ), + contentPadding: MySpacing.all(16), + ); + } + @override Widget build(BuildContext context) { - return SafeArea( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), + return Obx(() { + return BaseBottomSheet( + title: "Reimbursement Info", + isSubmitting: controller.isLoading.value, + onCancel: () { + widget.onClose(); + Navigator.pop(context); + }, + onSubmit: () async { + if (commentCtrl.text.trim().isEmpty || + txnCtrl.text.trim().isEmpty || + dateStr.value.isEmpty || + controller.selectedReimbursedBy.value == null) { + Get.snackbar("Incomplete", "Please fill all fields"); + return; + } + + final success = await widget.onSubmit( + comment: commentCtrl.text.trim(), + reimburseTransactionId: txnCtrl.text.trim(), + reimburseDate: dateStr.value, + reimburseById: controller.selectedReimbursedBy.value!.id, + statusId: widget.statusId, + ); + + if (success) { + Get.back(); + Get.snackbar('Success', 'Reimbursement submitted'); + } else { + Get.snackbar('Error', controller.errorMessage.value); + } + }, child: Column( - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Drag handle - Container( - width: 50, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(20), + MyText.labelMedium("Comment"), + MySpacing.height(8), + TextField( + controller: commentCtrl, + decoration: _inputDecoration("Enter comment"), + ), + MySpacing.height(16), + + MyText.labelMedium("Transaction ID"), + MySpacing.height(8), + TextField( + controller: txnCtrl, + decoration: _inputDecoration("Enter transaction ID"), + ), + MySpacing.height(16), + + MyText.labelMedium("Reimbursement Date"), + MySpacing.height(8), + GestureDetector( + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: dateStr.value.isEmpty + ? DateTime.now() + : DateFormat('yyyy-MM-dd').parse(dateStr.value), + firstDate: DateTime(2020), + lastDate: DateTime(2100), + ); + if (picked != null) { + dateStr.value = DateFormat('yyyy-MM-dd').format(picked); + } + }, + child: AbsorbPointer( + child: TextField( + controller: TextEditingController(text: dateStr.value), + decoration: _inputDecoration("Select Date").copyWith( + suffixIcon: const Icon(Icons.calendar_today), + ), + ), ), ), + MySpacing.height(16), - // Title - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.titleLarge('Reimbursement Info', fontWeight: 700), - const SizedBox(), - ], - ), - const SizedBox(height: 20), - - Flexible( - child: SingleChildScrollView( - child: Column( - children: [ - _buildInputField(label: 'Comment', controller: commentCtrl), - const SizedBox(height: 16), - _buildInputField( - label: 'Transaction ID', controller: txnCtrl), - const SizedBox(height: 16), - _buildDatePickerField(), - const SizedBox(height: 16), - _buildEmployeePickerField(), - const SizedBox(height: 24), - _buildActionButtons(), - ], + MyText.labelMedium("Reimbursed By"), + MySpacing.height(8), + GestureDetector( + onTap: _showEmployeeList, + child: AbsorbPointer( + child: TextField( + controller: TextEditingController( + text: controller.selectedReimbursedBy.value == null + ? "" + : '${controller.selectedReimbursedBy.value?.firstName ?? ''} ${controller.selectedReimbursedBy.value?.lastName ?? ''}', + ), + decoration: _inputDecoration("Select Employee").copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), ), ), ), ], ), - ), - ); - } - - Widget _buildInputField( - {required String label, required TextEditingController controller}) { - return TextField( - controller: controller, - decoration: InputDecoration( - labelText: label, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), - ); - } - - Widget _buildDatePickerField() { - return Obx(() { - return InkWell( - onTap: () async { - final picked = await showDatePicker( - context: context, - initialDate: dateStr.value.isEmpty - ? DateTime.now() - : DateFormat('yyyy-MM-dd').parse(dateStr.value), - firstDate: DateTime(2020), - lastDate: DateTime(2100), - ); - if (picked != null) { - dateStr.value = DateFormat('yyyy-MM-dd').format(picked); - } - }, - child: InputDecorator( - decoration: InputDecoration( - labelText: 'Reimbursement Date', - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), - child: Row( - children: [ - Icon(Icons.date_range, color: Colors.grey.shade600), - const SizedBox(width: 10), - Text( - dateStr.value.isEmpty ? "Select Date" : dateStr.value, - style: TextStyle( - fontSize: 14, - color: dateStr.value.isEmpty ? Colors.grey : Colors.black, - ), - ), - ], - ), - ), ); }); } - - Widget _buildEmployeePickerField() { - return Obx(() { - return GestureDetector( - onTap: _showEmployeeList, - child: InputDecorator( - decoration: InputDecoration( - labelText: 'Reimbursed By', - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), - child: Text( - controller.selectedReimbursedBy.value == null - ? "Select Reimbursed By" - : '${controller.selectedReimbursedBy.value?.firstName ?? ''} ${controller.selectedReimbursedBy.value?.lastName ?? ''}', - style: TextStyle( - fontSize: 14, - color: controller.selectedReimbursedBy.value == null - ? Colors.grey - : Colors.black, - ), - ), - ), - ); - }); - } - - Widget _buildActionButtons() { - return Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () { - widget.onClose(); - Get.back(); - }, - icon: const Icon(Icons.close, color: Colors.white), - label: MyText.bodyMedium("Cancel", - color: Colors.white, fontWeight: 600), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 7), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Obx(() { - return ElevatedButton.icon( - onPressed: controller.isLoading.value - ? null - : () async { - if (commentCtrl.text.trim().isEmpty || - txnCtrl.text.trim().isEmpty || - dateStr.value.isEmpty || - controller.selectedReimbursedBy.value == null) { - Get.snackbar("Incomplete", "Please fill all fields"); - return; - } - - final success = await widget.onSubmit( - comment: commentCtrl.text.trim(), - reimburseTransactionId: txnCtrl.text.trim(), - reimburseDate: dateStr.value, - reimburseById: - controller.selectedReimbursedBy.value!.id, - statusId: widget.statusId, - ); - - if (success) { - Get.back(); - Get.snackbar('Success', 'Reimbursement submitted'); - } else { - Get.snackbar('Error', controller.errorMessage.value); - } - }, - icon: const Icon(Icons.check_circle_outline, color: Colors.white), - label: MyText.bodyMedium( - controller.isLoading.value ? "Submitting..." : "Submit", - color: Colors.white, - fontWeight: 600, - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - padding: - const EdgeInsets.symmetric(horizontal: 5, vertical: 7), - ), - ); - }), - ), - ], - ); - } } diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index 2877090..86fb009 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -39,7 +39,7 @@ class ExpenseDetailScreen extends StatelessWidget { Widget build(BuildContext context) { final controller = Get.put(ExpenseDetailController()); final projectController = Get.find(); - controller.fetchExpenseDetails(expenseId); + controller.init(expenseId); return Scaffold( backgroundColor: const Color(0xFFF7F7F7), @@ -90,27 +90,15 @@ class ExpenseDetailScreen extends StatelessWidget { ), body: SafeArea( child: Obx(() { - // Show error snackbar only once after frame render - if (controller.errorMessage.isNotEmpty) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Get.snackbar( - "Error", - controller.errorMessage.value, - backgroundColor: Colors.red.withOpacity(0.9), - colorText: Colors.white, - ); - controller.errorMessage.value = ''; - }); - } - if (controller.isLoading.value) { return _buildLoadingSkeleton(); } final expense = controller.expense.value; - if (expense == null) { + + if (controller.errorMessage.isNotEmpty || expense == null) { return Center( - child: MyText.bodyMedium("No expense details found."), + child: MyText.bodyMedium("No data to display."), ); } @@ -221,7 +209,6 @@ class ExpenseDetailScreen extends StatelessWidget { }) async { final success = await controller .updateExpenseStatusWithReimbursement( - expenseId: expense.id, comment: comment, reimburseTransactionId: reimburseTransactionId, reimburseDate: reimburseDate, @@ -236,7 +223,8 @@ class ExpenseDetailScreen extends StatelessWidget { backgroundColor: Colors.green.withOpacity(0.8), colorText: Colors.white, ); - await controller.fetchExpenseDetails(expenseId); + await controller.fetchExpenseDetails(); + return true; } else { Get.snackbar( @@ -251,8 +239,9 @@ class ExpenseDetailScreen extends StatelessWidget { ), ); } else { - final success = await controller.updateExpenseStatus( - expense.id, next.id); + final success = + await controller.updateExpenseStatus(next.id); + if (success) { Get.snackbar( 'Success', @@ -260,7 +249,7 @@ class ExpenseDetailScreen extends StatelessWidget { backgroundColor: Colors.green.withOpacity(0.8), colorText: Colors.white, ); - await controller.fetchExpenseDetails(expenseId); + await controller.fetchExpenseDetails(); } else { Get.snackbar( 'Error', diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index b5b6bf5..313d632 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -1,47 +1,13 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/employee_model.dart'; -/// Wrapper to open Expense Filter Bottom Sheet -void openExpenseFilterBottomSheet( - BuildContext context, ExpenseController expenseController) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return ExpenseFilterBottomSheetWrapper( - expenseController: expenseController); - }, - ); -} - -class ExpenseFilterBottomSheetWrapper extends StatelessWidget { - final ExpenseController expenseController; - - const ExpenseFilterBottomSheetWrapper( - {super.key, required this.expenseController}); - - @override - Widget build(BuildContext context) { - return DraggableScrollableSheet( - initialChildSize: 0.7, - minChildSize: 0.4, - maxChildSize: 0.95, - expand: false, - builder: (context, scrollController) { - return ExpenseFilterBottomSheet( - expenseController: expenseController, - scrollController: scrollController, - ); - }, - ); - } -} - class ExpenseFilterBottomSheet extends StatelessWidget { final ExpenseController expenseController; final ScrollController scrollController; @@ -52,26 +18,143 @@ class ExpenseFilterBottomSheet extends StatelessWidget { required this.scrollController, }); + InputDecoration _inputDecoration(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: MyTextStyle.bodySmall(xMuted: true), + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5), + ), + contentPadding: MySpacing.all(12), + ); + } + @override Widget build(BuildContext context) { return Obx(() { - return SafeArea( - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), + return BaseBottomSheet( + title: 'Filter Expenses', + onCancel: () => Get.back(), + onSubmit: () { + expenseController.fetchExpenses(); + Get.back(); + }, + submitText: 'Submit', + submitColor: Colors.indigo, + submitIcon: Icons.check_circle_outline, + child: SingleChildScrollView( + controller: scrollController, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: SingleChildScrollView( - controller: scrollController, - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 20), - child: _buildContent(context), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => expenseController.clearFilters(), + child: const Text( + "Reset Filter", + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ), ), ), - _buildBottomButtons(), + MySpacing.height(8), + + _buildField("Project", _popupSelector( + context, + currentValue: expenseController.selectedProject.value.isEmpty + ? 'Select Project' + : expenseController.selectedProject.value, + items: expenseController.globalProjects, + onSelected: (value) => + expenseController.selectedProject.value = value, + )), + MySpacing.height(16), + + _buildField("Expense Status", _popupSelector( + context, + currentValue: expenseController.selectedStatus.value.isEmpty + ? 'Select Expense Status' + : expenseController.expenseStatuses + .firstWhereOrNull((e) => + e.id == expenseController.selectedStatus.value) + ?.name ?? + 'Select Expense Status', + items: expenseController.expenseStatuses + .map((e) => e.name) + .toList(), + onSelected: (name) { + final status = expenseController.expenseStatuses + .firstWhere((e) => e.name == name); + expenseController.selectedStatus.value = status.id; + }, + )), + MySpacing.height(16), + + _buildField("Date Range", Row( + children: [ + Expanded(child: _dateButton( + label: expenseController.startDate.value == null + ? 'Start Date' + : DateTimeUtils.formatDate( + expenseController.startDate.value!, 'dd MMM yyyy'), + onTap: () async { + DateTime? picked = await showDatePicker( + context: context, + initialDate: + expenseController.startDate.value ?? DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null) { + expenseController.startDate.value = picked; + } + }, + )), + MySpacing.width(8), + Expanded(child: _dateButton( + label: expenseController.endDate.value == null + ? 'End Date' + : DateTimeUtils.formatDate( + expenseController.endDate.value!, 'dd MMM yyyy'), + onTap: () async { + DateTime? picked = await showDatePicker( + context: context, + initialDate: + expenseController.endDate.value ?? DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null) { + expenseController.endDate.value = picked; + } + }, + )), + ], + )), + MySpacing.height(16), + + _buildField("Paid By", _employeeSelector( + selectedEmployees: expenseController.selectedPaidByEmployees, + )), + MySpacing.height(16), + + _buildField("Created By", _employeeSelector( + selectedEmployees: expenseController.selectedCreatedByEmployees, + )), ], ), ), @@ -79,208 +162,17 @@ class ExpenseFilterBottomSheet extends StatelessWidget { }); } - /// Builds the filter content - Widget _buildContent(BuildContext context) { + Widget _buildField(String label, Widget child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: Container( - width: 50, - height: 4, - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(20), - ), - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.titleLarge('Filter Expenses', fontWeight: 700), - TextButton( - onPressed: () => expenseController.clearFilters(), - child: const Text( - "Reset Filter", - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - - const SizedBox(height: 16), - - /// Project Filter - _buildCardSection( - title: "Project", - child: _popupSelector( - context, - currentValue: expenseController.selectedProject.value.isEmpty - ? 'Select Project' - : expenseController.selectedProject.value, - items: expenseController.globalProjects, - onSelected: (value) => - expenseController.selectedProject.value = value, - ), - ), - const SizedBox(height: 16), - - /// Expense Status Filter - _buildCardSection( - title: "Expense Status", - child: _popupSelector( - context, - currentValue: expenseController.selectedStatus.value.isEmpty - ? 'Select Expense Status' - : expenseController.expenseStatuses - .firstWhereOrNull((e) => - e.id == expenseController.selectedStatus.value) - ?.name ?? - 'Select Expense Status', - items: - expenseController.expenseStatuses.map((e) => e.name).toList(), - onSelected: (name) { - final status = expenseController.expenseStatuses - .firstWhere((e) => e.name == name); - expenseController.selectedStatus.value = status.id; - }, - ), - ), - const SizedBox(height: 16), - - /// Date Range Filter - _buildCardSection( - title: "Date Range", - child: Row( - children: [ - Expanded( - child: _dateButton( - label: expenseController.startDate.value == null - ? 'Start Date' - : DateTimeUtils.formatDate( - expenseController.startDate.value!, 'dd MMM yyyy'), - onTap: () async { - DateTime? picked = await showDatePicker( - context: context, - initialDate: - expenseController.startDate.value ?? DateTime.now(), - firstDate: DateTime(2020), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) - expenseController.startDate.value = picked; - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _dateButton( - label: expenseController.endDate.value == null - ? 'End Date' - : DateTimeUtils.formatDate( - expenseController.endDate.value!, 'dd MMM yyyy'), - onTap: () async { - DateTime? picked = await showDatePicker( - context: context, - initialDate: - expenseController.endDate.value ?? DateTime.now(), - firstDate: DateTime(2020), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) - expenseController.endDate.value = picked; - }, - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - /// Paid By Filter - _buildCardSection( - title: "Paid By", - child: _employeeFilterSection( - selectedEmployees: expenseController.selectedPaidByEmployees, - ), - ), - const SizedBox(height: 16), - - /// Created By Filter - _buildCardSection( - title: "Created By", - child: _employeeFilterSection( - selectedEmployees: expenseController.selectedCreatedByEmployees, - ), - ), - const SizedBox(height: 24), + MyText.labelMedium(label), + MySpacing.height(8), + child, ], ); } - /// Bottom Action Buttons - Widget _buildBottomButtons() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - child: Row( - children: [ - // Cancel Button - Expanded( - child: ElevatedButton.icon( - onPressed: () { - Get.back(); - }, - icon: const Icon(Icons.close, color: Colors.white), - label: MyText.bodyMedium( - "Cancel", - color: Colors.white, - fontWeight: 600, - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 7), - ), - ), - ), - const SizedBox(width: 12), - - // Submit Button - Expanded( - child: ElevatedButton.icon( - onPressed: () { - expenseController.fetchExpenses(); - Get.back(); - }, - icon: const Icon(Icons.check_circle_outline, color: Colors.white), - label: MyText.bodyMedium( - "Submit", - color: Colors.white, - fontWeight: 600, - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 7), - ), - ), - ), - ], - ), - ); - } - - /// Popup Selector Widget _popupSelector( BuildContext context, { required String currentValue, @@ -288,21 +180,20 @@ class ExpenseFilterBottomSheet extends StatelessWidget { required ValueChanged onSelected, }) { return PopupMenuButton( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onSelected: onSelected, - itemBuilder: (context) { - return items - .map((e) => PopupMenuItem( - value: e, - child: Text(e), - )) - .toList(); - }, + itemBuilder: (context) => items + .map((e) => PopupMenuItem( + value: e, + child: Text(e), + )) + .toList(), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: MySpacing.all(12), decoration: BoxDecoration( + color: Colors.grey.shade100, border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -321,55 +212,52 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ); } - /// Card Section Wrapper - Widget _buildCardSection({required String title, required Widget child}) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium(title, fontWeight: 600), - const SizedBox(height: 6), - child, - ], - ); - } - - /// Date Button Widget _dateButton({required String label, required VoidCallback onTap}) { - return ElevatedButton.icon( - onPressed: onTap, - icon: const Icon(Icons.calendar_today, size: 16), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey.shade100, - foregroundColor: Colors.black, - elevation: 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + return GestureDetector( + onTap: onTap, + child: Container( + padding: MySpacing.xy(16, 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: [ + const Icon(Icons.calendar_today, size: 16, color: Colors.grey), + MySpacing.width(8), + Expanded( + child: Text(label, + style: MyTextStyle.bodyMedium(), + overflow: TextOverflow.ellipsis), + ), + ], + ), ), - label: Text(label, overflow: TextOverflow.ellipsis), ); } - /// Employee Filter Section - Widget _employeeFilterSection( - {required RxList selectedEmployees}) { + Widget _employeeSelector({ + required RxList selectedEmployees, + }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Obx(() { return Wrap( - spacing: 6, + spacing: 8, runSpacing: -8, children: selectedEmployees.map((emp) { return Chip( label: Text(emp.name), - deleteIcon: const Icon(Icons.close, size: 18), onDeleted: () => selectedEmployees.remove(emp), + deleteIcon: const Icon(Icons.close, size: 18), backgroundColor: Colors.grey.shade200, ); }).toList(), ); }), - const SizedBox(height: 6), + MySpacing.height(8), Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { @@ -387,19 +275,11 @@ class ExpenseFilterBottomSheet extends StatelessWidget { selectedEmployees.add(emp); } }, - fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) { + fieldViewBuilder: (context, controller, focusNode, _) { return TextField( controller: controller, focusNode: focusNode, - decoration: InputDecoration( - hintText: 'Search Employee', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), + decoration: _inputDecoration("Search Employee"), ); }, optionsViewBuilder: (context, onSelected, options) { @@ -411,7 +291,6 @@ class ExpenseFilterBottomSheet extends StatelessWidget { child: SizedBox( height: 200, child: ListView.builder( - padding: EdgeInsets.zero, itemCount: options.length, itemBuilder: (context, index) { final emp = options.elementAt(index); diff --git a/lib/view/expense/expense_screen.dart b/lib/view/expense/expense_screen.dart index 5b15f71..8436a2c 100644 --- a/lib/view/expense/expense_screen.dart +++ b/lib/view/expense/expense_screen.dart @@ -42,8 +42,9 @@ class _ExpenseMainScreenState extends State { isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) { - return ExpenseFilterBottomSheetWrapper( + return ExpenseFilterBottomSheet( expenseController: expenseController, + scrollController: ScrollController(), ); }, );