import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:generp/screens/hrm/TourExpensesDetailsScreen.dart'; import 'package:provider/provider.dart'; import '../../Utils/app_colors.dart'; import '../../Models/hrmModels/tourExpensesListResponse.dart'; import '../../Notifiers/hrmProvider/tourExpensesProvider.dart'; import 'AddTourExpBillScreen.dart'; class TourExpensesListScreen extends StatefulWidget { const TourExpensesListScreen({super.key}); @override State createState() => _TourExpensesListScreenState(); } class _TourExpensesListScreenState extends State { final ScrollController _scrollController = ScrollController(); int _currentPage = 1; bool _isLoadingMore = false; bool _hasMoreItems = true; @override void initState() { super.initState(); _scrollController.addListener(_scrollListener); // fetch first page only once Future.microtask(() { final provider = Provider.of(context, listen: false); provider.fetchTourExpenses(context, "1"); }); } @override void dispose() { _scrollController.removeListener(_scrollListener); _scrollController.dispose(); super.dispose(); } void _scrollListener() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { _loadMoreItems(context); } } void _loadMoreItems(BuildContext context) { if (_isLoadingMore || !_hasMoreItems) return; final provider = Provider.of(context, listen: false); setState(() { _isLoadingMore = true; _currentPage++; }); provider .fetchTourExpenses(context, _currentPage.toString(), append: true) .then((_) { setState(() { _isLoadingMore = false; final newItems = provider.response?.tourList ?? []; if (newItems.length < _currentPage * 10) { // assuming API gives 10 items per page _hasMoreItems = false; } }); }).catchError((_) { setState(() { _isLoadingMore = false; _currentPage--; // rollback }); }); } void _refreshList(BuildContext context) { final provider = Provider.of(context, listen: false); setState(() { _currentPage = 1; _hasMoreItems = true; _isLoadingMore = false; }); provider.fetchTourExpenses(context, "1"); } @override Widget build(BuildContext context) { return SafeArea( top: false, child: Consumer( builder: (context, provider, child) { return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, backgroundColor: Colors.white, title: Row( children: [ InkResponse( onTap: () => Navigator.pop(context, true), child: SvgPicture.asset( "assets/svg/appbar_back_button.svg", height: 25, ), ), const SizedBox(width: 10), const Text( "Tour Expenses", style: TextStyle( fontSize: 18, fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600, color: Colors.black87, ), ), ], ), ), backgroundColor: const Color(0xFFF6F6F8), body: _buildBody(provider), bottomNavigationBar: _buildBottomBar(context), ); }, ), ); } Widget _buildBody(TourExpensesProvider provider) { if (provider.isLoading && _currentPage == 1) { return const Center( child: CircularProgressIndicator(color: Colors.blue)); } if (provider.errorMessage != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(provider.errorMessage!), const SizedBox(height: 16), ElevatedButton( onPressed: () => _refreshList(context), child: const Text("Retry"), ), ], ), ); } if (provider.response?.tourList == null || provider.response!.tourList!.isEmpty) { return const Center(child: Text("No Tour Expenses Found")); } final list = provider.response!.tourList!; return RefreshIndicator( onRefresh: () async { _refreshList(context); }, child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(12), itemCount: list.length + (_hasMoreItems ? 1 : 0), itemBuilder: (context, index) { if (index >= list.length) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center( child: _isLoadingMore ? const CircularProgressIndicator(color: Colors.blue) : !_hasMoreItems ? const Text("No more items to load") : const SizedBox.shrink(), ), ); } final TourList item = list[index]; return _buildItem(context, item); }, ), ); } Widget _buildItem(BuildContext context, TourList item) { return InkWell( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => TourExpensesDetailsScreen(tourBillId: item.id.toString()), ), ); }, child: Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Container( height: 46, width: 46, decoration: BoxDecoration( color: _getAvatarColor(item.approvalStatus), shape: BoxShape.circle, ), child: Center( child: Text( getText(item.approvalStatus), style: TextStyle( fontSize: 15, fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w500, color: _getAvatarTxtColor(item.approvalStatus), ), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.placeOfVisit ?? "-", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: "JakartaRegular", fontSize: 14, color: AppColors.semi_black, ), ), Text( item.appliedDate ?? "-", style: TextStyle( fontFamily: "JakartaRegular", fontSize: 14, color: AppColors.grey_semi, ), ), ], ), ), Text( "₹${item.appliedAmount ?? '0'}", style: const TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: Color(0xff1487c9), ), ), ], ), ), ); } Widget _buildBottomBar(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), color: Colors.white, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xff1487c9), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), padding: const EdgeInsets.symmetric(vertical: 14), elevation: 0, ), onPressed: () { HapticFeedback.selectionClick(); Navigator.push( context, MaterialPageRoute( builder: (context) => const AddBillScreen(pageTitleName: "Add Bill"), settings: const RouteSettings(name: 'AddTourExpBillScreen'), ), ).then((_) { _refreshList(context); }); }, child: const Text( "Add Bill", style: TextStyle( fontSize: 16, fontFamily: "JakartaMedium", fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ); } /// Avatar color generator Color _getAvatarColor(value) { var color = AppColors.approved_bg_color; switch (value) { case 'HR Approved': return AppColors.approved_bg_color; case 'Expired at HR': return AppColors.rejected_bg_color; case 'Expired at TL': return AppColors.rejected_bg_color; } return color; } Color _getAvatarTxtColor(value) { var color = AppColors.approved_text_color; switch (value) { case 'HR Approved': return AppColors.approved_text_color; case 'Expired at HR': return AppColors.rejected_text_color; case 'Expired at TL': return AppColors.rejected_text_color; } return color; } String getText(value) { switch (value) { case 'HR Approved': return "A"; case 'Expired at HR': return "E"; case 'Expired at TL': return "E"; case 'Updated': return "U"; default: return "R"; } } }