import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/expense/expense_screen_controller.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_snackbar.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/model/expense/expense_list_model.dart'; import 'package:marco/view/expense/expense_detail_screen.dart'; import 'package:marco/model/expense/add_expense_bottom_sheet.dart'; import 'package:marco/view/expense/expense_filter_bottom_sheet.dart'; class ExpenseMainScreen extends StatefulWidget { const ExpenseMainScreen({super.key}); @override State createState() => _ExpenseMainScreenState(); } class _ExpenseMainScreenState extends State { bool isHistoryView = false; final searchController = TextEditingController(); final expenseController = Get.put(ExpenseController()); final projectController = Get.find(); @override void initState() { super.initState(); expenseController.fetchExpenses(); } void _refreshExpenses() => expenseController.fetchExpenses(); void _openFilterBottomSheet() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => ExpenseFilterBottomSheet( expenseController: expenseController, scrollController: ScrollController(), ), ); } List _getFilteredExpenses() { final query = searchController.text.trim().toLowerCase(); final now = DateTime.now(); final filtered = expenseController.expenses.where((e) { return query.isEmpty || e.expensesType.name.toLowerCase().contains(query) || e.supplerName.toLowerCase().contains(query) || e.paymentMode.name.toLowerCase().contains(query); }).toList() ..sort((a, b) => b.transactionDate.compareTo(a.transactionDate)); return isHistoryView ? filtered .where((e) => e.transactionDate.isBefore(DateTime(now.year, now.month))) .toList() : filtered .where((e) => e.transactionDate.month == now.month && e.transactionDate.year == now.year) .toList(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: _ExpenseAppBar(projectController: projectController), body: SafeArea( child: Column( children: [ _SearchAndFilter( controller: searchController, onChanged: (_) => setState(() {}), onFilterTap: _openFilterBottomSheet, onRefreshTap: _refreshExpenses, expenseController: expenseController, ), _ToggleButtons( isHistoryView: isHistoryView, onToggle: (v) => setState(() => isHistoryView = v), ), Expanded( child: Obx(() { if (expenseController.isLoading.value) { return SkeletonLoaders.expenseListSkeletonLoader(); } if (expenseController.errorMessage.isNotEmpty) { return Center( child: MyText.bodyMedium( expenseController.errorMessage.value, color: Colors.red, ), ); } final filteredList = _getFilteredExpenses(); return _ExpenseList( expenseList: filteredList, onViewDetail: () => expenseController.fetchExpenses(), ); }), ), ], ), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.red, onPressed: showAddExpenseBottomSheet, child: const Icon(Icons.add, color: Colors.white), ), ); } } class _ExpenseAppBar extends StatelessWidget implements PreferredSizeWidget { final ProjectController projectController; const _ExpenseAppBar({required this.projectController}); @override Size get preferredSize => const Size.fromHeight(72); @override Widget build(BuildContext context) { return 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, children: [ MyText.titleLarge('Expenses', fontWeight: 700), MySpacing.height(2), GetBuilder( builder: (_) { final name = projectController.selectedProject?.name ?? 'Select Project'; return Row( children: [ const Icon(Icons.work_outline, size: 14, color: Colors.grey), MySpacing.width(4), Expanded( child: MyText.bodySmall( name, overflow: TextOverflow.ellipsis, color: Colors.grey[700], fontWeight: 600, ), ), ], ); }, ), ], ), ), ], ), ), ); } } class _SearchAndFilter extends StatelessWidget { final TextEditingController controller; final ValueChanged onChanged; final VoidCallback onFilterTap; final VoidCallback onRefreshTap; final ExpenseController expenseController; const _SearchAndFilter({ required this.controller, required this.onChanged, required this.onFilterTap, required this.onRefreshTap, required this.expenseController, }); @override Widget build(BuildContext context) { return Padding( padding: MySpacing.fromLTRB(12, 10, 12, 0), child: Row( children: [ Expanded( child: SizedBox( height: 35, child: TextField( controller: controller, onChanged: onChanged, 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), Tooltip( message: 'Refresh Data', child: IconButton( icon: const Icon(Icons.refresh, color: Colors.green, size: 24), onPressed: onRefreshTap, ), ), MySpacing.width(4), Obx(() { return IconButton( icon: Stack( clipBehavior: Clip.none, children: [ const Icon(Icons.tune, color: Colors.black), if (expenseController.isFilterApplied) Positioned( top: -1, right: -1, child: Container( width: 10, height: 10, decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 1.5), ), ), ), ], ), onPressed: onFilterTap, ); }), ], ), ); } } class _ToggleButtons extends StatelessWidget { final bool isHistoryView; final ValueChanged onToggle; const _ToggleButtons({ required this.isHistoryView, required this.onToggle, }); @override Widget build(BuildContext context) { return Padding( padding: MySpacing.fromLTRB(8, 12, 8, 5), child: 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: [ _ToggleButton( label: 'Expenses', icon: Icons.receipt_long, selected: !isHistoryView, onTap: () => onToggle(false), ), _ToggleButton( label: 'History', icon: Icons.history, selected: isHistoryView, onTap: () => onToggle(true), ), ], ), ), ); } } class _ToggleButton extends StatelessWidget { final String label; final IconData icon; final bool selected; final VoidCallback onTap; const _ToggleButton({ required this.label, required this.icon, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { 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), MyText.bodyMedium(label, color: selected ? Colors.white : Colors.grey, fontWeight: 600), ], ), ), ), ); } } class _ExpenseList extends StatelessWidget { final List expenseList; final Future Function()? onViewDetail; const _ExpenseList({required this.expenseList, this.onViewDetail}); void _showDeleteConfirmation(BuildContext context, ExpenseModel expense) { final ExpenseController controller = Get.find(); final RxBool isDeleting = false.obs; showDialog( context: context, barrierDismissible: false, builder: (_) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Obx(() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), child: isDeleting.value ? const SizedBox( height: 100, child: Center(child: CircularProgressIndicator()), ) : Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.delete, size: 48, color: Colors.redAccent), const SizedBox(height: 16), MyText.titleLarge( "Delete Expense", fontWeight: 600, color: Theme.of(context).colorScheme.onBackground, ), const SizedBox(height: 12), MyText.bodySmall( "Are you sure you want to delete this draft expense?", textAlign: TextAlign.center, color: Theme.of(context) .colorScheme .onSurface .withOpacity(0.7), ), const SizedBox(height: 24), // Updated Button UI Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () => Navigator.pop(context), 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: 12), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: () async { isDeleting.value = true; await controller.deleteExpense(expense.id); isDeleting.value = false; Navigator.pop(context); showAppSnackbar( title: 'Deleted', message: 'Expense has been deleted.', type: SnackbarType.success, ); }, icon: const Icon(Icons.delete_forever, color: Colors.white), label: MyText.bodyMedium( "Delete", color: Colors.white, fontWeight: 600, ), style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), ], ), ], ), ); }), ), ); } @override Widget build(BuildContext context) { if (expenseList.isEmpty) { return Center(child: MyText.bodyMedium('No expenses found.')); } return ListView.separated( padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), itemCount: expenseList.length, separatorBuilder: (_, __) => Divider(color: Colors.grey.shade300, height: 20), itemBuilder: (context, index) { final expense = expenseList[index]; final formattedDate = DateTimeUtils.convertUtcToLocal( expense.transactionDate.toIso8601String(), format: 'dd MMM yyyy, hh:mm a', ); return GestureDetector( onTap: () async { final result = await Get.to( () => ExpenseDetailScreen(expenseId: expense.id), arguments: {'expense': expense}, ); if (result == true && onViewDetail != null) { await onViewDetail!(); } }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.bodyMedium(expense.expensesType.name, fontWeight: 600), Row( children: [ MyText.bodyMedium( '₹ ${expense.amount.toStringAsFixed(2)}', fontWeight: 600), if (expense.status.name.toLowerCase() == 'draft') ...[ const SizedBox(width: 8), GestureDetector( onTap: () => _showDeleteConfirmation(context, expense), child: const Icon(Icons.delete, color: Colors.red, size: 20), ), ], ], ), ], ), const SizedBox(height: 6), Row( children: [ MyText.bodySmall(formattedDate, fontWeight: 500), const Spacer(), MyText.bodySmall(expense.status.name, fontWeight: 500), ], ), ], ), ), ); }, ); } }