From 8c5035d67968b5fa080bf30d7ded79f28a978902 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 18 Jul 2025 17:51:59 +0530 Subject: [PATCH] feat(expense): add expense management screens and functionality --- .../assign_task_bottom_sheet .dart | 2 +- .../expense/add_expense_bottom_sheet.dart | 319 +++++++++++++ lib/routes.dart | 6 + .../Attendence/attendance_screen.dart | 10 +- lib/view/dashboard/dashboard_screen.dart | 7 +- lib/view/directory/directory_view.dart | 2 +- lib/view/expense/expense_detail_screen.dart | 241 ++++++++++ lib/view/expense/expense_screen.dart | 437 ++++++++++++++++++ 8 files changed, 1015 insertions(+), 9 deletions(-) create mode 100644 lib/model/expense/add_expense_bottom_sheet.dart create mode 100644 lib/view/expense/expense_detail_screen.dart create mode 100644 lib/view/expense/expense_screen.dart diff --git a/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart index a61dbac..2e52a8a 100644 --- a/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart @@ -135,7 +135,7 @@ class _AssignTaskBottomSheetState extends State { children: [ MyText.titleMedium("Select Team :", fontWeight: 600), const SizedBox(width: 4), - Icon(Icons.filter_alt, + Icon(Icons.tune, color: const Color.fromARGB(255, 95, 132, 255)), ], ), diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart new file mode 100644 index 0000000..3dfd6cb --- /dev/null +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -0,0 +1,319 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +void showAddExpenseBottomSheet() { + final TextEditingController amountController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController supplierController = TextEditingController(); + final TextEditingController transactionIdController = TextEditingController(); + final TextEditingController gstController = TextEditingController(); + + String selectedProject = "Select Project"; + String selectedCategory = "Select Expense Type"; + String selectedPaymentMode = "Select Payment Mode"; + String selectedLocation = "Select Location"; + bool preApproved = false; + + Get.bottomSheet( + StatefulBuilder( + builder: (context, setState) { + return SafeArea( + child: Padding( + padding: + const EdgeInsets.only(top: 60), + child: Material( + color: Colors.white, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(20)), + child: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height - 60, + ), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Drag Handle + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + // Title + Center( + child: MyText.titleLarge( + "Add Expense", + fontWeight: 700, + ), + ), + const SizedBox(height: 20), + + // Project + _sectionTitle(Icons.work_outline, "Project"), + const SizedBox(height: 6), + _dropdownTile( + title: selectedProject, + onTap: () { + setState(() { + selectedProject = "Project A"; + }); + }, + ), + const SizedBox(height: 16), + + // Expense Type + GST + _sectionTitle( + Icons.category_outlined, "Expense Type & GST No."), + const SizedBox(height: 6), + _dropdownTile( + title: selectedCategory, + onTap: () { + setState(() { + selectedCategory = "Travel Expense"; + }); + }, + ), + const SizedBox(height: 8), + _customTextField( + controller: gstController, + hint: "Enter GST No.", + ), + const SizedBox(height: 16), + + // Payment Mode + _sectionTitle(Icons.payment, "Payment Mode"), + const SizedBox(height: 6), + _dropdownTile( + title: selectedPaymentMode, + onTap: () { + setState(() { + selectedPaymentMode = "UPI"; + }); + }, + ), + const SizedBox(height: 16), + + // Paid By + _sectionTitle(Icons.person, "Paid By (Employee)"), + const SizedBox(height: 6), + _dropdownTile( + title: "Self (Default)", + onTap: () {}, + ), + const SizedBox(height: 16), + + // Transaction Date + _sectionTitle(Icons.calendar_today, "Transaction Date"), + const SizedBox(height: 6), + _dropdownTile( + title: "Select Date & Time", + onTap: () async { + // Add date/time picker + }, + ), + const SizedBox(height: 16), + + // Amount + _sectionTitle(Icons.currency_rupee, "Amount"), + const SizedBox(height: 6), + _customTextField( + controller: amountController, + hint: "Enter Amount", + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + + // Supplier + _sectionTitle(Icons.store_mall_directory, + "Supplier Name / Expense Done At"), + const SizedBox(height: 6), + _customTextField( + controller: supplierController, + hint: "Enter Supplier Name", + ), + const SizedBox(height: 16), + + // Location + _sectionTitle(Icons.location_on_outlined, "Location"), + const SizedBox(height: 6), + _dropdownTile( + title: selectedLocation, + onTap: () { + setState(() { + selectedLocation = "Pune"; + }); + }, + ), + const SizedBox(height: 16), + + // Description + _sectionTitle(Icons.description_outlined, "Description"), + const SizedBox(height: 6), + _customTextField( + controller: descriptionController, + hint: "Enter Description", + maxLines: 3, + ), + const SizedBox(height: 16), + + // Bill Attachment + _sectionTitle(Icons.attachment, "Bill Attachment"), + const SizedBox(height: 6), + OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.upload_file), + label: const Text("Upload Bill"), + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + const SizedBox(height: 16), + + // Transaction ID + _sectionTitle( + Icons.confirmation_num_outlined, "Transaction ID"), + const SizedBox(height: 6), + _customTextField( + controller: transactionIdController, + hint: "Enter Transaction ID", + ), + const SizedBox(height: 16), + + // Pre-Approved Switch + Row( + children: [ + Switch( + value: preApproved, + onChanged: (val) => + setState(() => preApproved = val), + activeColor: Colors.red, + ), + const SizedBox(width: 8), + const Text("Pre-Approved?"), + ], + ), + const SizedBox(height: 24), + + // Action Buttons + 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( + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + // Handle Save + Get.back(); + }, + icon: const Icon(Icons.check, size: 18), + label: MyText.bodyMedium( + "Submit", + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + isScrollControlled: true, + ); +} + +/// Section Title +Widget _sectionTitle(IconData icon, String title) { + return Row( + children: [ + Icon(icon, color: Colors.grey[700], size: 18), + const SizedBox(width: 8), + MyText.bodyMedium(title, fontWeight: 600), + ], + ); +} + +/// Custom TextField +Widget _customTextField({ + required TextEditingController controller, + required String hint, + int maxLines = 1, + TextInputType keyboardType = TextInputType.text, +}) { + return TextField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + decoration: InputDecoration( + hintText: hint, + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + ); +} + +/// Dropdown Tile +Widget _dropdownTile({required String title, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + title, + style: const TextStyle(fontSize: 14, color: Colors.black87), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ); +} diff --git a/lib/routes.dart b/lib/routes.dart index 391eeb9..4f27766 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -17,6 +17,7 @@ import 'package:marco/view/auth/login_option_screen.dart'; import 'package:marco/view/auth/mpin_screen.dart'; import 'package:marco/view/auth/mpin_auth_screen.dart'; import 'package:marco/view/directory/directory_main_screen.dart'; +import 'package:marco/view/expense/expense_screen.dart'; class AuthMiddleware extends GetMiddleware { @override @@ -65,6 +66,11 @@ getPageRoute() { name: '/dashboard/directory-main-page', page: () => DirectoryMainScreen(), middlewares: [AuthMiddleware()]), + // Expense + GetPage( + name: '/dashboard/expense-main-page', + page: () => ExpenseMainScreen(), + middlewares: [AuthMiddleware()]), // Authentication GetPage(name: '/auth/login', page: () => LoginScreen()), GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()), diff --git a/lib/view/dashboard/Attendence/attendance_screen.dart b/lib/view/dashboard/Attendence/attendance_screen.dart index 12d8e34..1f019ef 100644 --- a/lib/view/dashboard/Attendence/attendance_screen.dart +++ b/lib/view/dashboard/Attendence/attendance_screen.dart @@ -173,7 +173,7 @@ class _AttendanceScreenState extends State with UIMixin { final selectedProjectId = Get.find() .selectedProjectId - ?.value; + .value; final selectedView = result['selectedTab'] as String?; @@ -208,9 +208,9 @@ class _AttendanceScreenState extends State with UIMixin { child: Padding( padding: const EdgeInsets.all(8.0), child: Icon( - Icons.filter_list_alt, + Icons.tune, color: Colors.blueAccent, - size: 28, + size: 20, ), ), ), @@ -225,7 +225,7 @@ class _AttendanceScreenState extends State with UIMixin { onTap: () async { final projectId = Get.find() .selectedProjectId - ?.value; + .value; if (projectId != null && projectId.isNotEmpty) { try { await attendanceController @@ -244,7 +244,7 @@ class _AttendanceScreenState extends State with UIMixin { child: Icon( Icons.refresh, color: Colors.green, - size: 28, + size: 22, ), ), ), diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index de064f4..93d65b3 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -23,9 +23,10 @@ class DashboardScreen extends StatefulWidget { static const String attendanceRoute = "/dashboard/attendance"; static const String tasksRoute = "/dashboard/daily-task"; static const String dailyTasksRoute = "/dashboard/daily-task-planing"; - static const String dailyTasksProgressRoute = - "/dashboard/daily-task-progress"; + static const String dailyTasksProgressRoute = "/dashboard/daily-task-progress"; static const String directoryMainPageRoute = "/dashboard/directory-main-page"; + static const String expenseMainPageRoute = "/dashboard/expense-main-page"; + @override State createState() => _DashboardScreenState(); @@ -157,6 +158,8 @@ class _DashboardScreenState extends State with UIMixin { DashboardScreen.dailyTasksProgressRoute), _StatItem(LucideIcons.folder, "Directory", contentTheme.info, DashboardScreen.directoryMainPageRoute), + _StatItem(LucideIcons.badge_dollar_sign, "Expense", contentTheme.info, + DashboardScreen.expenseMainPageRoute), ]; return GetBuilder( diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 5e1bcd3..30c8caa 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -213,7 +213,7 @@ class _DirectoryViewState extends State { borderRadius: BorderRadius.circular(10), ), child: IconButton( - icon: Icon(Icons.filter_alt_outlined, + icon: Icon(Icons.tune, size: 20, color: isFilterActive ? Colors.indigo diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart new file mode 100644 index 0000000..6ad1194 --- /dev/null +++ b/lib/view/expense/expense_detail_screen.dart @@ -0,0 +1,241 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class ExpenseDetailScreen extends StatelessWidget { + const ExpenseDetailScreen({super.key}); + + Color _getStatusColor(String status) { + switch (status) { + case 'Request': + return Colors.blue; + case 'Review': + return Colors.orange; + case 'Approved': + return Colors.green; + case 'Paid': + return Colors.purple; + case 'Closed': + return Colors.grey; + default: + return Colors.black; + } + } + + @override + Widget build(BuildContext context) { + final Map expense = + Get.arguments['expense'] as Map; + final Color statusColor = _getStatusColor(expense['status']!); + final ProjectController projectController = Get.find(); + + return Scaffold( + backgroundColor: const Color(0xFFF7F7F7), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: Colors.white, + elevation: 1, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.back(), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + 'Expense Details', + fontWeight: 700, + color: Colors.black, + ), + MySpacing.height(2), + Obx(() { + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; + return InkWell( + onTap: () => Get.toNamed('/project-selector'), + child: Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ), + ); + }), + ], + ), + ), + ], + ), + ), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Card( + elevation: 6, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFFFF4B2B), Color(0xFFFF416C)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + expense['title'] ?? 'N/A', + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 6), + Text( + expense['amount'] ?? '₹ 0', + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.flag, size: 16, color: statusColor), + const SizedBox(width: 6), + Text( + expense['status'] ?? 'N/A', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + + // Details Section + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _detailRow(Icons.calendar_today, "Date & Time", + expense['date'] ?? 'N/A'), + _detailRow(Icons.category_outlined, "Expense Type", + "${expense['category']} (GST: ${expense['gstNo'] ?? 'N/A'})"), + _detailRow(Icons.payment, "Payment Mode", + expense['paymentMode'] ?? 'N/A'), + _detailRow(Icons.person, "Paid By", + expense['paidBy'] ?? 'N/A'), + _detailRow(Icons.access_time, "Transaction Date", + expense['transactionDate'] ?? 'N/A'), + _detailRow(Icons.location_on_outlined, "Location", + expense['location'] ?? 'N/A'), + _detailRow(Icons.store, "Supplier Name", + expense['supplierName'] ?? 'N/A'), + _detailRow(Icons.confirmation_num_outlined, + "Transaction ID", expense['transactionId'] ?? 'N/A'), + _detailRow(Icons.description, "Description", + expense['description'] ?? 'N/A'), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _detailRow(IconData icon, String title, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 20, color: Colors.grey[800]), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/expense/expense_screen.dart b/lib/view/expense/expense_screen.dart new file mode 100644 index 0000000..f2d0217 --- /dev/null +++ b/lib/view/expense/expense_screen.dart @@ -0,0 +1,437 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/model/expense/add_expense_bottom_sheet.dart'; +import 'package:marco/view/expense/expense_detail_screen.dart'; + +class ExpenseMainScreen extends StatefulWidget { + const ExpenseMainScreen({super.key}); + + @override + State createState() => _ExpenseMainScreenState(); +} + +class _ExpenseMainScreenState extends State { + final RxBool isHistoryView = false.obs; + final TextEditingController searchController = TextEditingController(); + final RxString searchQuery = ''.obs; + + final ProjectController projectController = Get.find(); + + final List> expenseList = [ + { + 'title': 'Travel Expense', + 'amount': '₹ 1,500', + 'status': 'Request', + 'date': '12 Jul 2025 • 3:45 PM', + 'category': 'Transport', + 'paymentMode': 'UPI', + 'transactionId': 'TXN123451' + }, + { + 'title': 'Hotel Stay', + 'amount': '₹ 4,500', + 'status': 'Approved', + 'date': '11 Jul 2025 • 9:30 AM', + 'category': 'Accommodation', + 'paymentMode': 'Credit Card', + 'transactionId': 'TXN123452' + }, + { + 'title': 'Food Bill', + 'amount': '₹ 1,200', + 'status': 'Paid', + 'date': '10 Jul 2025 • 7:10 PM', + 'category': 'Food', + 'paymentMode': 'Cash', + 'transactionId': 'TXN123453' + }, + ]; + + Color _getStatusColor(String status) { + switch (status) { + case 'Request': + return Colors.blue; + case 'Review': + return Colors.orange; + case 'Approved': + return Colors.green; + case 'Paid': + return Colors.purple; + case 'Closed': + return Colors.grey; + default: + return Colors.black; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: _buildAppBar(), + body: SafeArea( + child: Column( + children: [ + _buildSearchAndFilter(), + _buildToggleButtons(), + Expanded( + child: Obx( + () => isHistoryView.value + ? _buildHistoryList() + : _buildExpenseList(), + ), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + showAddExpenseBottomSheet(); + }, + backgroundColor: Colors.red, + child: const Icon(Icons.add, color: Colors.white), + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.offNamed('/dashboard'), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + 'Expenses', + fontWeight: 700, + color: Colors.black, + ), + MySpacing.height(2), + GetBuilder( + builder: (controller) { + final projectName = + controller.selectedProject?.name ?? 'Select Project'; + return InkWell( + onTap: () => Get.toNamed('/project-selector'), + child: Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildSearchAndFilter() { + return Padding( + padding: MySpacing.fromLTRB(12, 10, 12, 0), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 35, + child: TextField( + controller: searchController, + onChanged: (value) => searchQuery.value = value, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: const Icon(Icons.search, + size: 20, color: Colors.grey), + hintText: 'Search expenses...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + ), + ), + ), + MySpacing.width(8), + IconButton( + icon: const Icon(Icons.tune, color: Colors.black), + onPressed: _openFilterBottomSheet, + ), + ], + ), + ); + } + + Widget _buildToggleButtons() { + return Padding( + padding: MySpacing.fromLTRB(8, 12, 8, 5), + child: Obx(() { + return Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: const Color(0xFFF0F0F0), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + _buildToggleButton( + label: 'Expenses', + icon: Icons.receipt_long, + selected: !isHistoryView.value, + onTap: () => isHistoryView.value = false, + ), + _buildToggleButton( + label: 'History', + icon: Icons.history, + selected: isHistoryView.value, + onTap: () => isHistoryView.value = true, + ), + ], + ), + ); + }), + ); + } + + Widget _buildToggleButton({ + required String label, + required IconData icon, + required bool selected, + required VoidCallback onTap, + }) { + return Expanded( + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), + decoration: BoxDecoration( + color: selected ? Colors.red : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 16, color: selected ? Colors.white : Colors.grey), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + color: selected ? Colors.white : Colors.grey, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildExpenseList() { + return Obx(() { + final filteredList = expenseList.where((expense) { + return searchQuery.isEmpty || + expense['title']! + .toLowerCase() + .contains(searchQuery.value.toLowerCase()); + }).toList(); + + return _buildExpenseHistoryList(filteredList); + }); + } + + Widget _buildHistoryList() { + final historyList = expenseList + .where((item) => item['status'] == 'Paid' || item['status'] == 'Closed') + .toList(); + + return _buildExpenseHistoryList(historyList); + } + + Widget _buildExpenseHistoryList(List> list) { + return ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: list.length, + itemBuilder: (context, index) { + final item = list[index]; + return GestureDetector( + onTap: () => Get.to( + () => const ExpenseDetailScreen(), + arguments: {'expense': item}, + ), + child: _buildExpenseCard(item), + ); + }, + ); + } + + Widget _buildExpenseCard(Map item) { + final statusColor = _getStatusColor(item['status']!); + + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + margin: const EdgeInsets.symmetric(vertical: 8), + child: Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title & Amount Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(Icons.receipt_long, size: 20, color: Colors.red), + const SizedBox(width: 8), + Text( + item['title']!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Text( + item['amount']!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + ], + ), + const SizedBox(height: 8), + _buildInfoRow(Icons.calendar_today, item['date']!), + const SizedBox(height: 6), + _buildInfoRow(Icons.category_outlined, item['category']!), + const SizedBox(height: 6), + _buildInfoRow(Icons.payment, item['paymentMode']!), + _buildInfoRow(Icons.confirmation_num_outlined, item['transactionId']!), + const SizedBox(height: 10), + Align( + alignment: Alignment.centerRight, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + item['status']!, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow(IconData icon, String text) { + return Row( + children: [ + Icon(icon, size: 14, color: Colors.grey), + const SizedBox(width: 4), + Text( + text, + style: TextStyle(color: Colors.grey[600], fontSize: 12), + ), + ], + ); + } + + void _openFilterBottomSheet() { + Get.bottomSheet( + Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Wrap( + runSpacing: 10, + children: [ + const Text( + 'Filter Expenses', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + ListTile( + leading: const Icon(Icons.date_range), + title: const Text('Date Range'), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.work_outline), + title: const Text('Project'), + onTap: () {}, + ), + ListTile( + leading: const Icon(Icons.check_circle_outline), + title: const Text('Status'), + onTap: () {}, + ), + ], + ), + ), + ); + } +}