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'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.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(); final permissionController = Get.put(PermissionController()); @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) { final title = e.title?.toLowerCase() ?? ""; final payee = e.payee?.toLowerCase() ?? ""; return query.isEmpty || title.contains(query) || payee.contains(query); }).toList() ..sort((a, b) { final aDate = a.dueDate ?? DateTime(1900); final bDate = b.dueDate ?? DateTime(1900); return bDate.compareTo(aDate); }); DateTime startOfMonth = DateTime(now.year, now.month, 1); DateTime previousMonthEnd = DateTime(now.year, now.month, 0); return isHistory ? filtered.where((e) { final d = e.dueDate; return d != null && d.isBefore(startOfMonth); }).toList() : filtered.where((e) { final d = e.dueDate; return d != null && d.isAfter(previousMonthEnd); }).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: Obx(() { if (permissionController.permissions.isEmpty) { return const SizedBox.shrink(); } final canCreate = permissionController.hasPermission(Permissions.expenseUpload); return canCreate ? FloatingActionButton.extended( backgroundColor: contentTheme.primary, onPressed: showPaymentRequestBottomSheet, icon: const Icon(Icons.add, color: Colors.white), label: const Text( "Create Payment Request", style: TextStyle(color: Colors.white, fontSize: 16), ), ) : const SizedBox.shrink(); }), ); } 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); // ScrollController for infinite scroll final scrollController = ScrollController(); scrollController.addListener(() { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100 && !paymentController.isLoading.value) { paymentController.loadMorePaymentRequests(); } }); return MyRefreshIndicator( 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, padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), itemCount: list.length + 1, separatorBuilder: (_, __) => Divider(color: Colors.grey.shade300, height: 20), itemBuilder: (context, index) { if (index == list.length) { 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: () { 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), // ------------------------------- // ADV CHIP (only if advance) // ------------------------------- if (item.isAdvancePayment == true) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.2), borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.orange), ), child: const Text( "ADV", style: TextStyle( fontSize: 10, color: Colors.orange, fontWeight: FontWeight.bold, ), ), ), ], ], ), 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)}')) .withOpacity(0.5), borderRadius: BorderRadius.circular(5), ), child: MyText.bodySmall( item.expenseStatus.name, color: Colors.white, fontWeight: 500, ), ), ], ), ], ), ), ), ); } }