import 'package:dotted_line/dotted_line.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import '../../Notifiers/hrmProvider/tourExpensesDetailsProvider.dart'; import '../../Utils/app_colors.dart'; import '../finance/FileViewer.dart'; class TourExpensesDetailsScreen extends StatefulWidget { final String tourBillId; const TourExpensesDetailsScreen({Key? key, required this.tourBillId}) : super(key: key); @override State createState() => _TourExpensesDetailsScreenState(); } class _TourExpensesDetailsScreenState extends State{ @override Widget build(BuildContext context) { return SafeArea( top: false, child: ChangeNotifierProvider( create: (_) => TourExpensesDetailsProvider() ..fetchTourExpensesDetails(context, widget.tourBillId), child: Scaffold( appBar: AppBar( automaticallyImplyLeading: false, backgroundColor: Color(0xFFFFFFFF), title: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ InkResponse( onTap: () => Navigator.pop(context, true), child: SvgPicture.asset( "assets/svg/appbar_back_button.svg", height: 25, ), ), SizedBox(width: 10), InkResponse( onTap: () => Navigator.pop(context, true), child: Text( "Tour Expenses", style: TextStyle( fontSize: 18, height: 1.1, fontFamily: "Plus Jakarta Sans", fontWeight: FontWeight.w600, color: AppColors.semi_black, ), ), ), ], ), ), backgroundColor: AppColors.scaffold_bg_color, body: Consumer( builder: (context, provider, child) { if (provider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (provider.errorMessage != null) { return Center(child: Text(provider.errorMessage!)); } final response = provider.response; if (response == null) { return const Center(child: Text("No data available")); } debugPrint("==================requestDetails: ${widget.tourBillId}"); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ /// Header Card at the very top _expenseHeaderCard( title: response.requestDetails?.placeOfVisit ?? "Tour", date: response.requestDetails?.appliedDate ?? "-", status: (response.requestDetails?.approvalStatus?.isNotEmpty ?? false) ? response.requestDetails!.approvalStatus! : "No Status", details: [ {"key": "Employee", "value": response.requestDetails!.employeeName!}, {"key": "Approved By TL", "value": response.requestDetails!.tlApprovedBy!}, {"key": "TL Approval Amount", "value": response.requestDetails!.tlApprovedAmount!}, {"key": "TL Remarks", "value": response.requestDetails!.tlRemarks!}, {"key": "Approved By HR", "value": response.requestDetails!.hrApprovedBy!}, {"key": "HR Approval Amount", "value": response.requestDetails!.hrApprovedAmount!}, {"key": "TL Remarks", "value": response.requestDetails!.tlRemarks!}, {"key": "Total Approved Amount", "value": response.requestDetails?.approvedAmount ?? "-"}, {"key": "Total Balance Amount", "value": response.requestDetails!.appliedAmount!}, {"key": "Type", "value": response.requestDetails!.type!}, ], ), const SizedBox(height: 16), /// Tour Expense Card (Main Summary) if (response.requestDetails != null && response.tourExpenses != null) ...[ const SizedBox(height: 10), Padding( padding: const EdgeInsets.only(left: 30.0), child: Text("Tour Summary", style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.grey_thick, ), ), ), const SizedBox(height: 8), SizedBox( height: 321, // adjust height to match your card child: ListView( scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.05, ), children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.90, child: _tourExpenseCard( employeeName: response.requestDetails?.employeeName ?? "-", placeOfVisit: response.requestDetails?.placeOfVisit ?? "-", daAmount: response.tourExpenses?.da ?? "0", totalAmount: response.tourExpenses?.appliedAmount ?? "0", fromDate: response.tourExpenses?.fromDate ?? "-", toDate: response.tourExpenses?.toDate ?? "-", remarks: response.tourExpenses?.extraNote ?? "-", ), ), ], ), ), ], const SizedBox(height: 10), /// Travel Expenses Cards if (response.travelExpenses != null && response.travelExpenses!.isNotEmpty) ...[ const SizedBox(height: 10), Padding( padding: const EdgeInsets.only(left: 30.0), child: Text( "Travel Expenses", style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.grey_thick, ), ), ), const SizedBox(height: 8), SizedBox( height: 216, child: ListView.separated( scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.04, ), itemCount: response.travelExpenses!.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final t = response.travelExpenses![index]; return SizedBox( width: MediaQuery.of(context).size.width * 0.90, // card width child: _travelExpenseCard( travelType: t.travelType ?? "-", amount: t.fare ?? "0", from: t.froma ?? "-", to: t.toa ?? "-", imageUrl: t.imageDirFilePath ?? "", onViewTap: () { debugPrint("Open: ${t.imageDirFilePath}"); //Fileviewer(fileName: "", fileUrl: t.imageDirFilePath.toString()) Navigator.push( context, MaterialPageRoute( builder: ( context, ) => Fileviewer( fileName: t.imageDirFilePath ?? "", fileUrl: t.imageDirFilePath ?? "", ), ), ); }, ), ); }, ), ) ], const SizedBox(height: 10), /// Hotel Expenses Cards if (response.hotelExpenses != null && response.hotelExpenses!.isNotEmpty) ...[ const SizedBox(height: 10), Padding( padding: const EdgeInsets.only(left: 30.0), child: Text("Hotel Expenses", style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.grey_thick, ), ), ), const SizedBox(height: 8), SizedBox( height: 216, child: ListView.separated( scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.04, ), itemCount: response.hotelExpenses!.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final h = response.hotelExpenses![index]; return SizedBox( width: MediaQuery.of(context).size.width * 0.90, child: _hotelExpenseCard( hotelName: h.hotelName ?? "-", amount: h.amount ?? "0", fromDate: h.fromDate ?? "-", toDate: h.toDate ?? "-", imageUrl: h.imageDirFilePath ?? "", onViewTap: () { debugPrint("Open: ${h.imageDirFilePath}"); Navigator.push( context, MaterialPageRoute( builder: ( context, ) => Fileviewer( fileName: h.imageDirFilePath ?? "", fileUrl: h.imageDirFilePath ?? "", ), ), ); }, ), ); }, ), ), ], const SizedBox(height: 10), /// Other Expenses Cards if (response.otherExpenses != null && response.otherExpenses!.isNotEmpty) ...[ const SizedBox(height: 10), Padding( padding: const EdgeInsets.only(left: 30.0), child: Text("Other Expenses", style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.grey_thick, ), ), ), const SizedBox(height: 8), SizedBox( height: 216, child: ListView.separated( scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric( horizontal: MediaQuery.of(context).size.width * 0.04, ), itemCount: response.otherExpenses!.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (context, index) { final o = response.otherExpenses![index]; return SizedBox( width: MediaQuery.of(context).size.width * 0.90, child: _otherExpenseCard( description: o.otherDesc ?? "-", amount: o.otherAmount ?? "0", date: o.otherDate ?? "-", imageUrl: o.imageDirFilePath ?? "", onViewTap: () { debugPrint("Open: ${o.imageDirFilePath}"); Navigator.push( context, MaterialPageRoute( builder: ( context, ) => Fileviewer( fileName: o.imageDirFilePath ?? "", fileUrl: o.imageDirFilePath ?? "", ), ), ); }, ), ); }, ), ), ], const SizedBox(height: 25), ], ), ); }, ), ), ), ); } /// Attach your reusable card functions here Widget _tourExpenseCard({ required String employeeName, required String placeOfVisit, required String daAmount, required String totalAmount, required String fromDate, required String toDate, required String remarks, }) { // paste your same implementation here return Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), // boxShadow: [ // BoxShadow( // color: Colors.grey.withOpacity(0.1), // blurRadius: 6, // offset: const Offset(0, 3), // ) // ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( radius: 22.5, backgroundColor: const Color(0x00FFF3E0), child: SvgPicture.asset( "assets/svg/home/home_inventory_ic.svg", ), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(employeeName, style: TextStyle( fontSize: 14.5, fontFamily: "JakartaMedium", fontWeight: FontWeight.w600, color: AppColors.semi_black, ) ), Text(placeOfVisit, style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.app_blue, ), ), ], ), ], ), const SizedBox(height: 12), _buildSectionHeader("Amount Details"), _buildKeyValue("DA Amount", daAmount), _buildKeyValue("Total Amount", totalAmount), const SizedBox(height: 10), _buildSectionHeader("Tour Time"), _buildKeyValue("From Date", fromDate), _buildKeyValue("To Date", toDate), const SizedBox(height: 10), _buildSectionHeader("Remarks"), _buildKeyValue("Extra Note", remarks), ], ), ); } Widget _travelExpenseCard({ required String travelType, required String amount, required String from, required String to, required imageUrl, required VoidCallback onViewTap, }) { // paste your travel card code here return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), // boxShadow: [ // BoxShadow( // color: Colors.grey.withOpacity(0.1), // blurRadius: 6, // offset: const Offset(0, 3), // ) // ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ CircleAvatar( radius: 20, backgroundColor: const Color(0xffFFF3E0), child: SvgPicture.asset( _getTravelIcon(travelType), width: 28, height: 28, ), ), const SizedBox(width: 8), Text(travelType, style: TextStyle( fontSize: 14.5, fontFamily: "JakartaMedium", fontWeight: FontWeight.w600, color: AppColors.semi_black, ) ), ], ), Text("₹$amount", style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.app_blue, ), ), ], ), const SizedBox(height: 15), _buildSectionHeader("Travel Details"), const SizedBox(height: 4), _buildKeyValue("From", from), const SizedBox(height: 2), _buildKeyValue("To", to), const SizedBox(height: 2), if ( imageUrl != null && imageUrl!.isNotEmpty) _buildKeyValue("Image", "View", isLink: true, onTap: onViewTap), ], ), ); } Widget _hotelExpenseCard({ required String hotelName, required String amount, required String fromDate, required String toDate, required imageUrl, required VoidCallback onViewTap, }) { // paste your hotel card code here return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), // boxShadow: [ // BoxShadow( // color: Colors.grey.withOpacity(0.1), // blurRadius: 6, // offset: const Offset(0, 3), // ) // ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ CircleAvatar( radius: 20, backgroundColor: const Color(0xffFCE4EC), child: SvgPicture.asset( "assets/svg/hrm/hotel_ic.svg", ), ), const SizedBox(width: 8), Text(hotelName, style: TextStyle( fontSize: 14.5, fontFamily: "JakartaMedium", fontWeight: FontWeight.w600, color: AppColors.semi_black, ) ), ], ), Text("₹$amount", style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.app_blue, ), ), ], ), const SizedBox(height: 15), _buildSectionHeader("Living Details"), const SizedBox(height: 4), _buildKeyValue("From", fromDate), const SizedBox(height: 2), _buildKeyValue("To", toDate), const SizedBox(height: 2), if ( imageUrl != null && imageUrl!.isNotEmpty) _buildKeyValue("Image", "View", isLink: true, onTap: onViewTap), ], ), ); } Widget _otherExpenseCard({ required String description, required String amount, required String date, required imageUrl, required VoidCallback onViewTap, }) { // paste your other expense card code here return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), // boxShadow: [ // BoxShadow( // color: Colors.grey.withOpacity(0.1), // blurRadius: 6, // offset: const Offset(0, 3), // ) // ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ CircleAvatar( radius: 20, backgroundColor: const Color(0xffEDE7F6), child: SvgPicture.asset( "assets/svg/hrm/books_ic.svg", ), ), const SizedBox(width: 8), Text( description, style: TextStyle( fontSize: 14.5, fontFamily: "JakartaMedium", fontWeight: FontWeight.w600, color: AppColors.semi_black, ) ), ], ), Text("₹$amount", style: TextStyle( fontFamily: "JakartaMedium", fontSize: 14, color: AppColors.app_blue, ), ), ], ), const SizedBox(height: 15), _buildSectionHeader("Other Details"), const SizedBox(height: 4), _buildKeyValue("Date", date), const SizedBox(height: 2), _buildKeyValue("Description", description), const SizedBox(height: 2), if ( imageUrl != null && imageUrl!.isNotEmpty) _buildKeyValue("Image", "View", isLink: true, onTap: onViewTap), ], ), ); } Widget _buildSectionHeader(String title) { return Row( children: [ Text(title, style: const TextStyle( fontSize: 14, fontFamily: "JakartaSemiBold", ) ), const SizedBox(width: 16), Expanded( child: DottedLine( dashGapLength: 3, dashGapColor: Colors.white, dashColor: AppColors.grey_semi, dashLength: 2, lineThickness: 0.5, ), ) ], ); } Widget _buildKeyValue(String key, String value, {bool isLink = false, VoidCallback? onTap}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 3.5, horizontal: 2), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(key, style: TextStyle( fontFamily: "JakartaRegular", fontSize: 14, color: AppColors.semi_black, ), ), isLink ? GestureDetector( onTap: onTap, child: const Text("View", style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.blue)), ) : Text(value, style: const TextStyle( fontSize: 14, color: Color(0xFF818181), ), ), ], ), ); } Widget _expenseHeaderCard({ required String title, required String date, required status, required List> details, }) { bool showMore = false; return StatefulBuilder( builder: (context, setState) { return Card( margin: EdgeInsets.symmetric(horizontal: 0, vertical: 1.2), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30), ), ), elevation: 0, child: Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30), ), ), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14), child: Column( children: [ /// Header Row Row( children: [ Container( height: 45, width: 45, padding: const EdgeInsets.all(7.5), decoration: const BoxDecoration( color: Color(0xFFE6F6FF), shape: BoxShape.circle, ), child: SvgPicture.asset("assets/svg/hrm/tour_main_ic.svg"), ), const SizedBox(width: 10), Expanded( flex: 5, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( fontSize: 14, fontFamily: "JakartaRegular", color: Color(0xff2D2D2D) ) ), const SizedBox(height: 3), Text(date, style: const TextStyle( fontSize: 12, color: Color(0xff818181) ) ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: _getStatusBgColor(status), ), child: Text(status, style: TextStyle( fontSize: 14, fontFamily: "JakartaRegular", color: _getStatusTxtColor(status))), ) ], ), const SizedBox(height: 10), /// Expanded Section if (showMore) ...[ Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ const Text("Amount Details", style: TextStyle( fontSize: 14, fontFamily: "JakartaSemiBold", ) ), const SizedBox(width: 10), Expanded( child: DottedLine( dashGapLength: 4, dashGapColor: Colors.white, dashColor: AppColors.grey_semi, dashLength: 2, lineThickness: 0.5, ), ), ], ), ), const SizedBox(height: 6), Column( children: details.map((d) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( flex: 4, child: Text(d["key"] ?? "-", style: TextStyle( fontSize: 14, color: AppColors.semi_black, fontFamily: "JakartaRegular", ) ) ), Expanded( flex: 3, child: Text(d["value"] ?? "-", textAlign: TextAlign.right, style: const TextStyle( fontSize: 14, fontFamily: "JakartaRegular", color: Color(0xff818181) ) ) ), ], ), ); }).toList(), ), ], /// Toggle Button InkWell( onTap: () => setState(() => showMore = !showMore), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(showMore ? "Hide Details" : "View Details", style: TextStyle( fontSize: 14, fontFamily: "JakartaMedium", fontWeight: FontWeight.w500, color: AppColors.app_blue, ) ), const SizedBox(width: 6), Transform.flip( flipY: showMore, child: SvgPicture.asset( "assets/svg/arrow_dropdown.svg", height: 25, width: 20, color: AppColors.app_blue, ), ) ], ), ), ) ], ), ), ); }, ); } ///travel icons String _getTravelIcon(String? travelType) { switch (travelType?.toLowerCase()) { case "flight": return "assets/svg/hrm/airplane_ic.svg"; case "train": return "assets/svg/hrm/train_ic.svg"; case "bus": return "assets/svg/hrm/bus_ic.svg"; case "car": return "assets/svg/hrm/car_ic.svg"; case "auto": return "assets/svg/hrm/truck_ic.svg"; case "bike": return "assets/svg/hrm/motorcycle_ic.svg"; default: return "assets/svg/hrm/travel_ic.svg"; // fallback } } /// Avatar color generator Color _getStatusBgColor(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 _getStatusTxtColor(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; } 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 "Requested"; } } }