import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:taxglide/consts/app_style.dart'; import 'package:get/get.dart'; import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/comman_button.dart'; import 'package:taxglide/consts/details_webscokect.dart'; import 'package:taxglide/consts/download_helper.dart'; import 'package:taxglide/consts/local_store.dart'; import 'package:taxglide/consts/validation_popup.dart'; import 'package:taxglide/controller/api_contoller.dart'; import 'package:taxglide/controller/api_repository.dart'; import 'package:taxglide/model/detail_model.dart'; import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart'; import 'package:taxglide/view/Main_controller/main_controller.dart'; import 'package:taxglide/view/screens/history/completed_live_chat_screen.dart'; class DetailScreen extends ConsumerStatefulWidget { final int id; final int? sourceTabIndex; const DetailScreen({super.key, required this.id, this.sourceTabIndex}); @override ConsumerState createState() => _DetailScreenState(); } class _DetailScreenState extends ConsumerState { Map _downloadingFiles = {}; // Track each file separately bool _isDownloadingInvoice = false; String? _downloadedInvoicePath; bool _isWebSocketInitialized = false; // Add this flag @override void initState() { super.initState(); _initializeWebSocket(); // Initialize WebSocket } // Add this method Future _initializeWebSocket() async { if (_isWebSocketInitialized) return; try { String? userId = await LocalStore().getUserId(); if (userId != null && userId.isNotEmpty) { final wsService = DetailsWebscokect(); // Setup message handler to refresh count wsService.onMessageReceived = (message) { debugPrint("📨 WebSocket message received: $message"); // Refresh count when message arrives _refreshCount(); }; // Connect WebSocket await wsService.connect(userId: userId); _isWebSocketInitialized = true; debugPrint("✅ WebSocket initialized for detail screen"); } } catch (e) { debugPrint("❌ Failed to initialize WebSocket: $e"); } } // Add this method to refresh count void _refreshCount() { // Get the chatId from the detail model final detailAsync = ref.read(serviceDetailProvider(widget.id)); detailAsync.whenData((detailModel) { final chatId = detailModel.data?.chatId; if (chatId != null && chatId.isNotEmpty) { // Invalidate the count provider to trigger refresh ref.invalidate(countProvider(chatId)); debugPrint("🔄 Count refreshed for chatId: $chatId"); } }); } @override void dispose() { // Optionally disconnect WebSocket when leaving screen // DetailsWebscokect().disconnect(); super.dispose(); } Future _handleDownload(String url) async { if (_downloadingFiles[url] == true) return; setState(() { _downloadingFiles[url] = true; }); try { // Check if file exists, if yes open it, if no download it final result = await DownloadHelper.downloadFile(url); // No snackbar for downloads - only notification will show // For existing files (view mode), no messages shown at all // If there's an error, still show it if (mounted && result['success'] == false) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${result['error']}'), backgroundColor: Colors.red, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), ); } } finally { if (mounted) { setState(() { _downloadingFiles[url] = false; }); } } } Future _handleInvoiceDownload(String invoiceUrl) async { if (_isDownloadingInvoice) return; // Check if already downloaded final existingPath = await DownloadHelper.checkFileExists(invoiceUrl); if (existingPath != null) { setState(() { _downloadedInvoicePath = existingPath; }); // Open the existing file await DownloadHelper.openDownloadedFile(existingPath); return; } setState(() { _isDownloadingInvoice = true; }); try { final result = await DownloadHelper.downloadFile(invoiceUrl); if (mounted && result['success'] == true) { setState(() { _downloadedInvoicePath = result['filePath']; }); } else if (mounted && result['success'] == false) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Download failed: ${result['error']}'), backgroundColor: Colors.red, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), ); } } finally { if (mounted) { setState(() { _isDownloadingInvoice = false; }); } } } Widget _buildInvoiceDownloadButton(String? invoiceUrl, double width) { if (invoiceUrl == null || invoiceUrl.isEmpty) { return SizedBox( width: double.infinity, height: 57, child: ElevatedButton.icon( onPressed: null, icon: const Icon(Icons.error_outline, color: Colors.white, size: 22), label: Text( "Invoice Not Available", textAlign: TextAlign.center, style: AppTextStyles.regular.copyWith( fontSize: width * 0.045, height: 1.3, letterSpacing: 0.04 * 17.64, color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), disabledBackgroundColor: Colors.grey, ), ), ); } return FutureBuilder( future: DownloadHelper.checkFileExists(invoiceUrl), builder: (context, snapshot) { final isAlreadyDownloaded = snapshot.data != null || _downloadedInvoicePath != null; return SizedBox( width: double.infinity, height: 57, child: ElevatedButton.icon( onPressed: _isDownloadingInvoice ? null : () => _handleInvoiceDownload(invoiceUrl), icon: _isDownloadingInvoice ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Icon( isAlreadyDownloaded ? Icons.visibility : Icons.download_rounded, color: Colors.white, size: 22, ), label: Text( _isDownloadingInvoice ? "Downloading..." : isAlreadyDownloaded ? "View Invoice" : "Download Invoice", textAlign: TextAlign.center, style: AppTextStyles.regular.copyWith( fontSize: width * 0.045, height: 1.3, letterSpacing: 0.04 * 17.64, color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: isAlreadyDownloaded ? const Color(0xFF0C5A78) : const Color(0xFF1E780C), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), elevation: 7.8, shadowColor: const Color(0x66000000), disabledBackgroundColor: Colors.grey, ), ), ); }, ); } Widget _buildProformaDownloadButton(String? proforma) { final bool isDisabled = proforma == null || proforma.isEmpty; return FutureBuilder( future: isDisabled ? null : DownloadHelper.checkFileExists(proforma), builder: (context, snapshot) { final bool isDownloaded = snapshot.data != null; return GestureDetector( onTap: isDisabled ? null : () async { if (isDownloaded) { /// 👀 View Proforma (OPEN LOCAL FILE) await DownloadHelper.openDownloadedFile(snapshot.data!); } else { /// ⬇️ Download Proforma await _handleInvoiceDownload(proforma); } }, child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ /// 🔵 Icon Container( width: 38.45, height: 38.45, decoration: const BoxDecoration( color: Color(0xFF4000FF), shape: BoxShape.circle, ), child: Icon( isDownloaded ? Icons.visibility : Icons.download_rounded, color: Colors.white, size: 22, ), ), const SizedBox(width: 5), /// 🔤 Text Text( isDisabled ? "Proforma Not Available" : isDownloaded ? "View Proforma" : "Download Proforma", style: AppTextStyles.semiBold.copyWith( fontSize: 12, height: 1.3, letterSpacing: 0.04 * 14.47, color: isDisabled ? Colors.grey : const Color(0xFF4000FF), ), ), ], ), ); }, ); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; final width = size.width; final height = size.height; final validationPopup = ValidationPopup(); final detailAsync = ref.watch(serviceDetailProvider(widget.id)); // 🔄 Silent refresh when notification trigger changes ref.listen(notificationTriggerProvider, (previous, next) { if (previous != null && next != previous) { debugPrint("🔔 Silent refresh triggered for DetailScreen"); ref.refresh(serviceDetailProvider(widget.id)); // Also refresh unread count if available detailAsync.whenData((model) { final chatId = model.data?.chatId; if (chatId != null && chatId.isNotEmpty) { ref.refresh(countProvider(chatId)); } }); } }); return Scaffold( body: Container( height: height, width: width, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage(AppAssets.backgroundimages), fit: BoxFit.cover, ), ), child: SafeArea( child: detailAsync.when( skipLoadingOnRefresh: true, loading: () => const Center( child: CircularProgressIndicator(color: Colors.deepPurple), ), error: (e, _) => Center( child: Text( "Error: $e", style: AppTextStyles.regular.copyWith( color: Colors.red, fontSize: 16, ), ), ), data: (DetailModel detailModel) { final data = detailModel.data; if (data == null) { return const Center(child: Text("No details found")); } final countAsync = data.chatId != null && data.chatId!.isNotEmpty ? ref.watch(countProvider(data.chatId!)) : null; return SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ /// 🔹 Custom App Bar Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 21, ), child: SizedBox( height: 50, width: double.infinity, child: Stack( alignment: Alignment.center, children: [ // ✅ Center Title Text( "Service Details", textAlign: TextAlign.center, style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.055, color: const Color(0xFF111827), ), ), // ✅ Left Back Icon Positioned( left: 0, child: GestureDetector( onTap: () { final targetTab = widget.sourceTabIndex ?? 0; Get.offAll( () => MainController(initialIndex: targetTab), ); }, child: const Icon( Icons.arrow_back_ios_rounded, color: Color(0xFF111827), ), ), ), if (data.serviceStatus == 'Completed') Positioned( right: 0, child: GestureDetector( onTap: () { Get.to( () => CompletedLiveChatScreen( fileid: data.id.toString(), chatid: int.tryParse( data.completedChatId ?? '0', ) ?? 0, ), )?.then((_) { ref .read( notificationTriggerProvider .notifier, ) .state++; ref.invalidate(chatMessagesProvider); }); }, child: Container( width: 39.0, height: 39.0, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all( color: const Color(0xFF055976), width: 0.8, ), ), child: const Icon( Icons.message_rounded, size: 18, ), ), ), ), ], ), ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Detailed Information", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.047, height: 1.4, letterSpacing: 0.03 * 20, ), textAlign: TextAlign.center, ), if (data.serviceStatus == 'Waiting for Admin' || data.serviceStatus == 'Payment Pending' || data.serviceStatus == 'Cancelled') Container( width: width * 0.3, height: height * 0.04, decoration: BoxDecoration( color: const Color(0xFFFFE8E8), borderRadius: BorderRadius.circular(5.52), border: Border.all( color: const Color(0xFFFFD7D7), width: 1.84, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.25), blurRadius: 7.17, offset: const Offset(0, 3.68), ), ], ), child: Center( child: Text( data.serviceStatus.toString(), style: AppTextStyles.regular.copyWith( fontSize: width * 0.029, color: const Color(0xFFFF0F0F), ), ), ), ), // 🔹 In Progress Badge if (data.serviceStatus == 'In Progress') Container( width: width * 0.25, height: height * 0.042, decoration: BoxDecoration( color: const Color(0xFFEAFAE6), borderRadius: BorderRadius.circular(6.21), border: Border.all( color: const Color(0xFFDDFFDD), width: 2.07, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.25), blurRadius: 8.08, offset: const Offset(0, 4.14), ), ], ), child: Center( child: Text( data.serviceStatus.toString(), style: AppTextStyles.regular.copyWith( fontSize: width * 0.032, letterSpacing: 0.03, color: const Color(0xFF12800C), ), ), ), ), // 🔹 Completed Badge if (data.serviceStatus == 'Completed') Container( width: width * 0.23, height: height * 0.038, decoration: BoxDecoration( color: const Color(0xFFFAF7E6), borderRadius: BorderRadius.circular(5.52), border: Border.all( color: const Color(0xFFFFE9DD), width: 1.84, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.25), blurRadius: 7.17, offset: const Offset(0, 3.68), ), ], ), child: Center( child: Text( data.serviceStatus.toString(), style: AppTextStyles.regular.copyWith( fontSize: width * 0.029, letterSpacing: 0.03, color: const Color(0xFFFF630F), ), ), ), ), ], ), ), if (data.serviceStatus == "Completed") ...[ Padding( padding: const EdgeInsets.symmetric( vertical: 20, horizontal: 20, ), child: Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all( color: const Color(0xFFE1E1E1), width: 1, ), boxShadow: const [ BoxShadow( color: Color(0x66757575), blurRadius: 15, offset: Offset(0, 1), ), ], ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "This is your final task document", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.042, height: 1.4, letterSpacing: 0.03 * 16, color: const Color(0xFF111827), ), textAlign: TextAlign.start, ), const SizedBox(height: 15), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: RichText( overflow: TextOverflow.ellipsis, text: TextSpan( children: [ TextSpan( text: 'Date: ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.createdDate ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), ), const SizedBox(width: 10), Flexible( child: RichText( overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, text: TextSpan( children: [ TextSpan( text: 'Payment : ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.paymentStatus ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), ), ], ), const SizedBox(height: 15), if (data.userHandoverDocuments.isNotEmpty) SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: data.userHandoverDocuments.length, itemBuilder: (context, index) { final doc = data.userHandoverDocuments[index]; final filePath = doc.filePath ?? ''; final isImage = filePath.toLowerCase().endsWith( '.jpg', ) || filePath.toLowerCase().endsWith( '.jpeg', ) || filePath.toLowerCase().endsWith( '.png', ) || filePath.toLowerCase().endsWith( '.gif', ) || filePath.toLowerCase().endsWith( '.webp', ); final isPdf = filePath .toLowerCase() .endsWith('.pdf'); final isDownloadingThis = _downloadingFiles[filePath] == true; return Padding( padding: const EdgeInsets.only( right: 10, ), child: GestureDetector( onTap: () => _handleDownload(filePath), child: Container( height: 90, width: 90, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: const Color(0xFFF5F5F5), boxShadow: [ BoxShadow( color: Colors.black .withOpacity(0.05), blurRadius: 3, offset: const Offset(0, 2), ), ], ), child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular( 8, ), child: isImage ? Image.network( filePath, width: 90, height: 90, fit: BoxFit.cover, errorBuilder: ( context, error, stackTrace, ) { return const Center( child: Icon( Icons .broken_image, color: Colors .grey, ), ); }, ) : isPdf ? const Center( child: Icon( Icons .picture_as_pdf, color: Colors.red, size: 50, ), ) : const Center( child: Icon( Icons .insert_drive_file, color: Colors.grey, size: 50, ), ), ), Center( child: Container( width: 56.12, height: 56.12, decoration: BoxDecoration( color: Colors.white .withOpacity(0.37), borderRadius: BorderRadius.circular( 8, ), ), child: Center( child: Container( width: 36.66, height: 36.66, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular( 6, ), boxShadow: const [ BoxShadow( color: Colors .black12, blurRadius: 4, offset: Offset( 0, 2, ), ), ], ), child: isDownloadingThis ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors .deepPurple, ), ) : FutureBuilder< String? >( future: DownloadHelper.checkFileExists( filePath, ), builder: ( context, snapshot, ) { final fileExists = snapshot.data != null; return Icon( fileExists ? Icons.visibility : Icons.download, color: Colors.black87, size: 20, ); }, ), ), ), ), ), ], ), ), ), ); }, ), ) else const SizedBox.shrink(), ], ), ), ), ), ], Column( children: [ if (data.paymentStatus == "Un Paid" && !(data.serviceStatus ?? "") .toLowerCase() .contains("cancelled")) ...[ Padding( padding: const EdgeInsets.symmetric( vertical: 31, horizontal: 20, ), child: Column( children: [ Container( width: double.infinity, decoration: BoxDecoration( color: const Color(0xFFFFF6ED), borderRadius: BorderRadius.circular(6), border: Border.all( color: const Color(0xFFD8CEC5), width: 1, ), boxShadow: const [ BoxShadow( color: Color(0x66716E6E), blurRadius: 15.2, offset: Offset(0, 1), ), ], ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Payment Advice", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.042, height: 1.4, letterSpacing: 0.03 * 16, color: const Color(0xFF111827), ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: RichText( overflow: TextOverflow.ellipsis, text: TextSpan( children: [ TextSpan( text: 'Date: ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.createdDate ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), ), const SizedBox(width: 10), Flexible( child: RichText( overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, text: TextSpan( children: [ TextSpan( text: 'Payment : ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.paymentStatus ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), ), ], ), const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Total Amount", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.04, color: const Color(0xFF111827), ), ), Text( "₹ ${data.paymentAmount ?? '0'}", style: AppTextStyles.semiBold.copyWith( fontFamily: 'Roboto', fontSize: width * 0.045, color: const Color(0xFF111827), ), ), ], ), ], ), ), ), const SizedBox(height: 20), CommanButton(text: 'Pay Now', onPressed: () {}), ], ), ), ] else if (data.paymentStatus == "Paid") ...[ Padding( padding: const EdgeInsets.symmetric( vertical: 31, horizontal: 20, ), child: Column( children: [ Container( width: double.infinity, decoration: BoxDecoration( color: const Color(0xFFFFF6ED), borderRadius: BorderRadius.circular(6), border: Border.all( color: const Color(0xFFD8CEC5), width: 1, ), boxShadow: const [ BoxShadow( color: Color(0x66716E6E), blurRadius: 15.2, offset: Offset(0, 1), ), ], ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Payment Advice", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.042, height: 1.4, letterSpacing: 0.03 * 16, color: const Color(0xFF111827), ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: RichText( overflow: TextOverflow.ellipsis, text: TextSpan( children: [ TextSpan( text: 'Date: ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.createdDate ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), ), const SizedBox(width: 10), Flexible( child: RichText( overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, text: TextSpan( children: [ TextSpan( text: 'Payment: ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.paymentStatus ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), ), ], ), const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Total Amount", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.04, color: const Color(0xFF111827), ), ), Text( "₹ ${data.paymentAmount ?? '0'}", style: AppTextStyles.semiBold.copyWith( fontFamily: 'Roboto', fontSize: width * 0.045, color: const Color(0xFF111827), ), ), ], ), ], ), ), ), const SizedBox(height: 20), _buildInvoiceDownloadButton( data.invoice, width, ), ], ), ), ] else ...[ const SizedBox.shrink(), ], if (data.proforma != null && data.proforma!.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric( vertical: 31, horizontal: 20, ), child: Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, // #FFFFFF borderRadius: BorderRadius.circular(10), border: Border.all( color: const Color(0xFFE1E1E1), width: 1, ), boxShadow: const [ BoxShadow( color: Color(0x40757575), // #75757540 offset: Offset(0, 1), blurRadius: 15, spreadRadius: 0, ), ], ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Profoma Advice", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.042, height: 1.4, letterSpacing: 0.03 * 16, color: const Color(0xFF111827), ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: RichText( overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, text: TextSpan( children: [ TextSpan( text: 'proforma Number : ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color( 0xFF111827, ), ), ), TextSpan( text: data.proformaNumber ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color( 0xFF111827, ), ), ), ], ), ), ), ], ), const SizedBox(height: 10), RichText( overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, text: TextSpan( children: [ TextSpan( text: 'proforma Status : ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.proformaStatus ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), const SizedBox(height: 20), RichText( overflow: TextOverflow.ellipsis, textAlign: TextAlign.end, text: TextSpan( children: [ TextSpan( text: 'Subject : ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), TextSpan( text: data.subject ?? "-", style: AppTextStyles.regular.copyWith( fontSize: width * 0.035, height: 1.3, letterSpacing: 0.04 * 13.97, color: const Color(0xFF111827), ), ), ], ), ), const SizedBox(height: 20), if (data.proformaStatus != "Accepted") ...[ Text( "Note : Once you accept this proforma only you can pay the amount for the service", textAlign: TextAlign.start, style: AppTextStyles.semiBold.copyWith( fontSize: 13, height: 1.78, // 178% line-height letterSpacing: 0.04 * 13, // 4% color: Colors.red, ), ), const SizedBox(height: 20), ], Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ /// 📄 Proforma (More width) Expanded( flex: 3, child: _buildProformaDownloadButton( data.proforma, ), ), const SizedBox(width: 8), if (data.proformaStatus != "Accepted") /// ✅ Accept (Less width) Expanded( flex: 2, child: SizedBox( height: 42, // match proforma height child: CommanButton( text: 'Accept', onPressed: () async { try { await ApiRepository() .proformaaccept( proformaId: int.parse( data.proformaId .toString(), ), ); validationPopup .showSuccessMessage( context, "Proforma accepted successfully", ); ref.invalidate( serviceDetailProvider, ); ref.invalidate( serviceHistoryNotifierProvider, ); } catch (error) { validationPopup.showErrorMessage( context, error is Map && error['error'] != null ? error['error'] : "Failed to accept proforma", ); } }, ), ), ), ], ), const SizedBox(height: 20), ], ), ), ), ), ], ], ), /// 🔹 Main Detail Box Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 12, ), child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow("File ID", "${data.id}", width), const SizedBox(height: 16), _buildInfoRow( "Request Type", "${data.service}", width, ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildInfoRow( "Date", "${data.createdDate}", width, ), ), const SizedBox(width: 10), Expanded( child: _buildInfoRow( "Time", "${data.createdTime}", width, ), ), ], ), const SizedBox(height: 16), _buildInfoRow( "Created by", "${data.createdBy}", width, ), const SizedBox(height: 16), _buildInfoRow("Message", "${data.message}", width), const SizedBox(height: 16), /// 🔹 File List Section Text( "File Attachments:", style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.04, color: const Color(0xFF111827), ), ), const SizedBox(height: 10), if (data.userUploadedDocuments.isNotEmpty) SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: data.userUploadedDocuments.length, itemBuilder: (context, index) { final doc = data.userUploadedDocuments[index]; final filePath = doc.filePath ?? ''; final isImage = filePath.toLowerCase().endsWith( '.jpg', ) || filePath.toLowerCase().endsWith( '.jpeg', ) || filePath.toLowerCase().endsWith( '.png', ) || filePath.toLowerCase().endsWith( '.gif', ) || filePath.toLowerCase().endsWith( '.webp', ); final isPdf = filePath .toLowerCase() .endsWith('.pdf'); final isDownloadingThis = _downloadingFiles[filePath] == true; return Padding( padding: const EdgeInsets.only(right: 10), child: GestureDetector( onTap: () => _handleDownload(filePath), child: Container( height: 90, width: 90, decoration: BoxDecoration( borderRadius: BorderRadius.circular( 8, ), color: const Color(0xFFF5F5F5), boxShadow: [ BoxShadow( color: Colors.black.withOpacity( 0.05, ), blurRadius: 3, offset: const Offset(0, 2), ), ], ), child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: isImage ? Image.network( filePath, width: 90, height: 90, fit: BoxFit.cover, errorBuilder: ( context, error, stackTrace, ) { return const Center( child: Icon( Icons .broken_image, color: Colors .grey, ), ); }, ) : isPdf ? const Center( child: Icon( Icons.picture_as_pdf, color: Colors.red, size: 50, ), ) : const Center( child: Icon( Icons .insert_drive_file, color: Colors.grey, size: 50, ), ), ), Center( child: Container( width: 56.12, height: 56.12, decoration: BoxDecoration( color: Colors.white .withOpacity(0.37), borderRadius: BorderRadius.circular( 8, ), ), child: Center( child: Container( width: 36.66, height: 36.66, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular( 6, ), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset( 0, 2, ), ), ], ), child: isDownloadingThis ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors .deepPurple, ), ) : FutureBuilder< String? >( future: DownloadHelper.checkFileExists( filePath, ), builder: (context, snapshot) { final fileExists = snapshot .data != null; return Icon( fileExists ? Icons .visibility : Icons .download, color: Colors .black87, size: 20, ); }, ), ), ), ), ), ], ), ), ), ); }, ), ) else Text( "No documents uploaded", style: AppTextStyles.regular.copyWith( color: const Color(0xFF6B7280), ), ), ], ), ), ), if (data.chatId != null && data.chatId!.isNotEmpty && !(data.serviceStatus ?? "").toLowerCase().contains("cancelled")) Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 20, ), child: countAsync?.when( data: (count) => CommanButton( text: "Chat", backgroundImage: AppAssets.backgroundimages, prefixIcon: messageIconWithBadge(count.count), onPressed: () => Get.to( () => LiveChatScreen( fileid: data.id.toString(), chatid: int.parse( data.chatId.toString(), ), ), )?.then((_) { ref .read( notificationTriggerProvider .notifier, ) .state++; ref.invalidate(chatMessagesProvider); }), ), loading: () => CommanButton( text: "Chat", backgroundImage: AppAssets.backgroundimages, prefixIcon: messageIconWithBadge(0), onPressed: () => Get.to( () => LiveChatScreen( fileid: data.id.toString(), chatid: int.parse( data.chatId.toString(), ), ), )?.then((_) { ref .read( notificationTriggerProvider .notifier, ) .state++; ref.invalidate(chatMessagesProvider); }), ), error: (_, __) => CommanButton( text: "Chat", backgroundImage: AppAssets.backgroundimages, prefixIcon: messageIconWithBadge(0), onPressed: () => Get.to( () => LiveChatScreen( fileid: data.id.toString(), chatid: int.parse( data.chatId.toString(), ), ), )?.then((_) { ref .read( notificationTriggerProvider .notifier, ) .state++; ref.invalidate(chatMessagesProvider); }), ), ) ?? CommanButton( text: "Chat", backgroundImage: AppAssets.backgroundimages, prefixIcon: messageIconWithBadge(0), onPressed: () => Get.to( () => LiveChatScreen( fileid: data.id.toString(), chatid: int.parse(data.chatId.toString()), ), )?.then((_) { ref .read( notificationTriggerProvider.notifier, ) .state++; ref.invalidate(chatMessagesProvider); }), ), ), SizedBox(height: 80.0 + MediaQuery.of(context).padding.bottom), ], ), ); }, ), ), ), ); } /// 🔹 Helper Widget for Each Info Row Widget _buildInfoRow(String label, String value, double width) { return RichText( text: TextSpan( children: [ TextSpan( text: '$label: ', style: AppTextStyles.semiBold.copyWith( fontSize: width * 0.04, color: const Color(0xFF111827), ), ), TextSpan( text: value.isEmpty ? '—' : value, style: AppTextStyles.medium.copyWith( fontSize: width * 0.04, height: 1.5, color: const Color(0xFF374151), ), ), ], ), ); } Widget messageIconWithBadge(int count) { return Stack( clipBehavior: Clip.none, children: [ const Icon(Icons.message_outlined, color: Colors.white, size: 26), if (count > 0) Positioned( right: -2, top: -2, child: Container( padding: const EdgeInsets.all(3), decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: Text( count.toString(), style: AppTextStyles.bold.copyWith( color: Colors.white, fontSize: 10, ), ), ), ), ], ); } }