import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/controller/finance/payment_request_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/model/finance/payment_request_filter_bottom_sheet.dart'; import 'package:marco/model/finance/add_payment_request_bottom_sheet.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/view/finance/payment_request_detail_screen.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class PaymentRequestMainScreen extends StatefulWidget { const PaymentRequestMainScreen({super.key}); @override State createState() => _PaymentRequestMainScreenState(); } class _PaymentRequestMainScreenState extends State with SingleTickerProviderStateMixin, UIMixin { late TabController _tabController; final searchController = TextEditingController(); final paymentController = Get.put(PaymentRequestController()); final projectController = Get.find(); @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); WidgetsBinding.instance.addPostFrameCallback((_) { paymentController.fetchPaymentRequests(); }); } @override void dispose() { _tabController.dispose(); super.dispose(); } Future _refreshPaymentRequests() async { await paymentController.fetchPaymentRequests(); } void _openFilterBottomSheet() { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) => PaymentRequestFilterBottomSheet( controller: paymentController, scrollController: ScrollController(), ), ); } List filteredList({required bool isHistory}) { final query = searchController.text.trim().toLowerCase(); final now = DateTime.now(); final filtered = paymentController.paymentRequests.where((e) { return query.isEmpty || e.title.toLowerCase().contains(query) || e.payee.toLowerCase().contains(query); }).toList() ..sort((a, b) => b.dueDate.compareTo(a.dueDate)); return isHistory ? filtered .where((e) => e.dueDate.isBefore(DateTime(now.year, now.month))) .toList() : filtered .where((e) => e.dueDate.month == now.month && e.dueDate.year == now.year) .toList(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: _buildAppBar(), body: Column( children: [ Container( color: Colors.white, child: TabBar( controller: _tabController, labelColor: Colors.black, unselectedLabelColor: Colors.grey, indicatorColor: Colors.red, tabs: const [ Tab(text: "Current Month"), Tab(text: "History"), ], ), ), Expanded( child: Container( color: Colors.grey[100], child: Column( children: [ _buildSearchBar(), Expanded( child: TabBarView( controller: _tabController, children: [ _buildPaymentRequestList(isHistory: false), _buildPaymentRequestList(isHistory: true), ], ), ), ], ), ), ), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: () { showPaymentRequestBottomSheet(); }, backgroundColor: contentTheme.primary, icon: const Icon(Icons.add), label: const Text("Create Payment Request"), ), ); } 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( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), onPressed: () => Get.offNamed('/dashboard/finance'), ), MySpacing.width(8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ MyText.titleLarge( 'Payment Requests', fontWeight: 700, color: Colors.black, ), 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, fontWeight: 600, overflow: TextOverflow.ellipsis, color: Colors.grey[700], ), ), ], ); }, ), ], ), ), ], ), ), ), ); } Widget _buildSearchBar() { return Padding( padding: MySpacing.fromLTRB(12, 10, 12, 0), child: Row( children: [ Expanded( child: SizedBox( height: 35, child: TextField( controller: searchController, onChanged: (_) => setState(() {}), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12), prefixIcon: const Icon(Icons.search, size: 20, color: Colors.grey), hintText: 'Search payment requests...', filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: contentTheme.primary, width: 1.5), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), ), ), ), MySpacing.width(4), Obx(() { return IconButton( icon: Stack( clipBehavior: Clip.none, children: [ const Icon(Icons.tune, color: Colors.black), if (paymentController.isFilterApplied.value) 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: _openFilterBottomSheet, ); }), ], ), ); } Widget _buildPaymentRequestList({required bool isHistory}) { return Obx(() { if (paymentController.isLoading.value && paymentController.paymentRequests.isEmpty) { return SkeletonLoaders.paymentRequestListSkeletonLoader(); } final list = filteredList(isHistory: isHistory); // Single ScrollController for this list final scrollController = ScrollController(); // Load more when reaching near bottom scrollController.addListener(() { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100 && !paymentController.isLoading.value) { paymentController.loadMorePaymentRequests(); } }); return RefreshIndicator( onRefresh: _refreshPaymentRequests, child: list.isEmpty ? ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.5, child: Center( child: Text( paymentController.errorMessage.isNotEmpty ? paymentController.errorMessage.value : "No payment requests found", style: const TextStyle(color: Colors.grey), ), ), ), ], ) : ListView.separated( controller: scrollController, // attach controller padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), itemCount: list.length + 1, // extra item for loading separatorBuilder: (_, __) => Divider(color: Colors.grey.shade300, height: 20), itemBuilder: (context, index) { if (index == list.length) { // Show loading indicator at bottom return Obx(() => paymentController.isLoading.value ? const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator()), ) : const SizedBox.shrink()); } final item = list[index]; return _buildPaymentRequestTile(item); }, ), ); }); } Widget _buildPaymentRequestTile(dynamic item) { final dueDate = DateTimeUtils.formatDate(item.dueDate, DateTimeUtils.defaultFormat); return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () { // Navigate to detail screen, passing the payment request ID Get.to(() => PaymentRequestDetailScreen(paymentRequestId: item.id)); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ MyText.bodyMedium(item.expenseCategory.name, fontWeight: 600), ], ), const SizedBox(height: 6), Row( children: [ MyText.bodySmall("Payee: ", color: Colors.grey[600]), MyText.bodySmall(item.payee, fontWeight: 600), ], ), const SizedBox(height: 6), Row( children: [ Row( children: [ MyText.bodySmall("Due Date: ", color: Colors.grey[600]), MyText.bodySmall(dueDate, fontWeight: 600), ], ), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Color(int.parse( '0xff${item.expenseStatus.color.substring(1)}')), borderRadius: BorderRadius.circular(5), ), child: MyText.bodySmall( item.expenseStatus.name, color: Colors.white, fontWeight: 500, ), ), ], ), ], ), ), ), ); } }