import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; import 'package:nhancepolicy/responsive.dart'; import 'package:nhancepolicy/service/api_service.dart'; import 'package:nhancepolicy/service/file_upload_service.dart'; import 'package:nhancepolicy/service/multi_file_upload_widget.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:file_picker/file_picker.dart'; import 'package:pdf/widgets.dart' as pw; import '../../../config/environment.dart'; import '../../../customAppBar/toastHelper.dart'; import 'package:nhancepolicy/logger.dart'; class NonEBClaimsHistory extends StatefulWidget { final String ticket_id; final String empName; final String policyType; final String? clientPolicyNo; final String? claimNo; final String postToken; const NonEBClaimsHistory({ Key? key, required this.ticket_id, required this.empName, required this.policyType, this.clientPolicyNo, this.claimNo, required this.postToken, }) : super(key: key); @override State createState() => _NonEBClaimsHistoryState(); } class _NonEBClaimsHistoryState extends State with TickerProviderStateMixin { late ApiService apiService; List> getClaimsHistoryList = []; List stepKeys = []; late Map stepMap; bool isLoading = false; dynamic _token; String? selectedFileNames; // html.File? uploadedFile; // List uploadedFiles = []; List uploadedFiles = []; final FileUploadService fileService = FileUploadService(); List claimFiles = []; List> requiredDocsList = []; List> requiredDocsListBackup = []; bool isSubmitting = false; bool isActionFreeze = false; bool useStatusKeyedTicketData = false; // IR Docs state bool showIRDocs = false; // final List> irDocList = [ // {"title": "Hospital Bill", "checked": false}, // {"title": "Discharge Summary", "checked": false}, // {"title": "Prescription", "checked": false}, // ]; final Map _assignedFiles = {}; @override void initState() { super.initState(); apiService = ApiService(context); // debug prints kept logDebug("CLAIMHISTORY"); logDebug(widget.claimNo); logDebug(widget.clientPolicyNo); logDebug(widget.policyType); logDebug(widget.ticket_id); getClaimsHistoryDetails(); } @override void dispose() { super.dispose(); } // ------------------------- // File pick (web + mobile) // ------------------------- Future pickFile() async { final result = await FilePicker.platform .pickFiles(withData: true, allowMultiple: false); if (result != null && result.files.isNotEmpty) { return result.files.first; } return null; } // ------------------------- // Assign same picked file to all checked docs // ------------------------- Future handleUploadForSelected() async { final checked = requiredDocsList.where((d) => d['document_received'] == true).toList(); if (checked.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Please select at least one document"))); return; } final picked = await pickFile(); if (picked == null) return; setState(() { for (var d in checked) { final title = d['document_name'] as String; _assignedFiles[title] = picked; } }); } // ------------------------- // Remove assignment // ------------------------- void removeAssignedFile(String title) { setState(() { _assignedFiles[title] = null; }); } // ------------------------- // Submit IR Docs (placeholder) // Replace with real API upload logic // ------------------------- Future submitIRDocs() async { setState(() => isSubmitting = true); // 🔥 start loader try { logDebug('enter'); // STEP 1: Must select at least one document type final selectedDocs = requiredDocsList .where((d) => d['document_received'] == true) .toList(); if (selectedDocs.isEmpty) { ToastHelper.showErrorToast( context, "Please select at least one IR document type"); return; } // STEP 2: Must upload at least one file if (!MultiFileUploadWidget.hasFiles || fileService.files.isEmpty) { ToastHelper.showErrorToast( context, "Please upload at least one document"); return; } // STEP 3: All uploaded files must have a label for (final uf in fileService.files) { if ((uf.label ?? "").trim().isEmpty) { ToastHelper.showErrorToast( context, "Please enter name for all uploaded documents"); return; } } // STEP 4: Prepare multipart request final url = Uri.parse("${Environment.apiUrlPost}api/v1/non-eb-claim/upload-required-doc"); final request = http.MultipartRequest('POST', url); request.headers['Authorization'] = "Bearer ${widget.postToken}"; request.headers['APP-SIGNATURE'] = 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y'; final requiredDocsPayload = { "is_action_freeze": isActionFreeze, "docs": requiredDocsList.map((d) { return { "document_name": d["document_name"], "document_received": d["document_received"], }; }).toList(), }; request.fields["required_docs"] = jsonEncode(requiredDocsPayload); // Add mapped fields if required request.fields['ticket_id'] = widget.ticket_id; // STEP 5: Convert image → PDF and attach files for (var uf in fileService.files) { final pf = uf.file; final ext = pf.extension?.toLowerCase() ?? ""; Uint8List fileBytes = pf.bytes!; // if (['jpg', 'jpeg', 'png', 'heic'].contains(ext)) { // final pdf = pw.Document(); // final image = pw.MemoryImage(fileBytes); // // pdf.addPage( // pw.Page( // build: (context) => pw.Center(child: pw.Image(image)), // ), // ); // // fileBytes = await pdf.save(); // // final pdfName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf'); // // request.files.add(http.MultipartFile.fromBytes( // "claim_docs[]", // fileBytes, // filename: pdfName, // )); // } else { request.files.add(http.MultipartFile.fromBytes( "claim_docs[]", fileBytes, filename: pf.name, )); // } } // STEP 6: Add names for these IR docs final labels = fileService.files.map((f) => f.label.trim()).toList(); request.fields['claim_doc_names'] = jsonEncode(labels); // Debug logDebug( "Files uploaded: ${fileService.files.map((e) => e.file.name).toList()}"); logDebug("Labels: $labels"); final stringFields = request.fields.map((key, value) => MapEntry(key, value?.toString() ?? '')); logDebug("📁 stringFields: ${stringFields}"); // STEP 7: Send the request final response = await request.send(); final responseBody = await response.stream.bytesToString(); final decoded = jsonDecode(responseBody); if (decoded["status"] == true) { ToastHelper.showSuccessToast(context, decoded["message"]); _resetIRDocs(); await getClaimsHistoryDetails(); // <--- refresh checkbox state from API setState(() => showIRDocs = false); } else { ToastHelper.showErrorToast( context, "Upload failed: ${decoded['message']}"); } } catch (e) { ToastHelper.showErrorToast(context, "Upload failed"); } finally { setState(() => isSubmitting = false); // 🔥 stop loader } } // ------------------------- // Existing API call (unchanged) // ------------------------- Future getClaimsHistoryDetails() async { setState(() { isLoading = true; }); try { final response = await apiService.getNonEBClaimsHistoryToApi( widget.ticket_id, widget.postToken); if (response['status'] == 'success' || response['status'] == true) { setState(() { isLoading = false; // New response shape support: // { // claims_data: {...}, // required_docs: {...}, // claims_files: [...], // ticket_data: [{status, changed_at}] // } // Also keep fallback for previous nested data response. final root = Map.from(response); final nestedData = root['data'] is Map ? Map.from(root['data']) : {}; final ticketDataRaw = root['ticket_data'] ?? nestedData['ticket_data']; if (ticketDataRaw is List) { useStatusKeyedTicketData = false; getClaimsHistoryList = ticketDataRaw .map>((e) => Map.from(e)) .toList(); stepMap = getClaimsHistoryList.isNotEmpty ? getClaimsHistoryList.first : {}; stepKeys = List.generate(getClaimsHistoryList.length, (i) => '$i'); } else if (ticketDataRaw is Map) { final map = Map.from(ticketDataRaw); if (map.containsKey('status')) { useStatusKeyedTicketData = false; getClaimsHistoryList = [map]; stepMap = map; stepKeys = const ['0']; } else { useStatusKeyedTicketData = true; stepMap = map; stepKeys = map.keys.map((k) => k.toString()).toList(); getClaimsHistoryList = [map]; } } else { useStatusKeyedTicketData = false; getClaimsHistoryList = []; stepMap = {}; stepKeys = []; } final claimsDocs = root['claims_files'] ?? root['claim_files'] ?? nestedData['claims_files'] ?? nestedData['claim_files']; claimFiles = claimsDocs is List ? List>.from(claimsDocs) : >[]; final requiredDocsRoot = root['required_docs'] ?? nestedData['required_docs']; if (requiredDocsRoot is Map) { isActionFreeze = requiredDocsRoot['is_action_freeze'] ?? false; final requiredDocs = requiredDocsRoot['docs']; requiredDocsList = requiredDocs is List ? List>.from(requiredDocs) : >[]; } else { isActionFreeze = false; requiredDocsList = >[]; } // ⭐ Make a backup copy to restore later requiredDocsListBackup = requiredDocsList .map((doc) => { "document_name": doc["document_name"], "document_received": doc["document_received"], }) .toList(); logDebug(isActionFreeze); logDebug('requiredDocsList $requiredDocsList'); for (var d in requiredDocsList) { _assignedFiles[d['document_name']] = null; } }); } else { setState(() => isLoading = false); logDebug('Request failed with status: ${response['code']}'); } } catch (e) { setState(() => isLoading = false); logDebug('Exception occurred: $e'); } } Future _launchURL(String url) async { final Uri uri = Uri.parse(url); try { await launchUrl(uri, mode: LaunchMode.externalApplication); } catch (e) { logDebug('Could not launch URL: $e'); } } void _resetIRDocs() { // Restore checkbox values from API backup requiredDocsList = requiredDocsListBackup .map((doc) => { "document_name": doc["document_name"], "document_received": doc["document_received"], }) .toList(); // Clear assigned uploaded files _assignedFiles.updateAll((key, value) => null); // Reset multi-file upload widget service fileService.clearAll(); MultiFileUploadWidget.hasFiles = false; setState(() {}); } // ------------------------- // Build // ------------------------- @override Widget build(BuildContext context) { // responsive decisions final mediaW = MediaQuery.of(context).size.width; final isMobile = mediaW < 600; final panelWidth = isMobile ? MediaQuery.of(context).size.width * 0.95 : 400.0; final keyValueWidgets = [ _buildKeyValue( 'Name', '${widget.empName ?? ''}'), SizedBox( width: Responsive.isDesktop(context) ? 16 : 0, height: Responsive.isDesktop(context) ? 0 : 8), _buildKeyValue('Policy Name', '${widget.policyType ?? ''} - ${widget.clientPolicyNo ?? ''}'), ]; final keyValueWidgetRow = [ // _buildKeyValue( // 'Claims Amount', // widget.claimAmount == null || widget.claimAmount!.trim().isEmpty // ? 'N/A' // : '₹${widget.claimAmount}'), // SizedBox( // width: Responsive.isDesktop(context) ? 16 : 0, // height: Responsive.isDesktop(context) ? 0 : 8), _buildKeyValue( 'Claims Number', widget.claimNo == null || widget.claimNo!.trim().isEmpty ? 'N/A' : widget.claimNo!), ]; return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { if (didPop) return; Navigator.pop(context); }, child: Container( // keep popup sized; adjust if required constraints: BoxConstraints(maxWidth: 1080, maxHeight: 820), padding: EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header row (keeps IR docs icon + close inside popup header) Row( children: [ Expanded( child: Text( 'Claims History', style: GoogleFonts.poppins( fontSize: Responsive.isDesktop(context) ? 20 : 16, fontWeight: FontWeight.w500, color: Color(0xFF101010), ), ), ), // IR Docs icon if (!isActionFreeze && requiredDocsList.isNotEmpty) MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { setState(() { showIRDocs = true; }); }, child: Container( height: 34, width: 34, margin: EdgeInsets.only(right: 8), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Color(0xFFBCBCBC)), borderRadius: BorderRadius.circular(6), ), child: Icon(Icons.folder_open, size: 20, color: Color(0xFF00A5A8)), ), ), ), // Close popup MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () => Navigator.pop(context), child: Container( height: 30, width: 30, decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Color(0xFFBCBCBC)), borderRadius: BorderRadius.circular(6), ), child: Icon(Icons.close, size: 25, color: Color(0xFFBCBCBC)), ), ), ) ], ), SizedBox(height: Responsive.isDesktop(context) ? 10 : 6), // BODY: for mobile we will show either the Claim content or full IR Docs content (switch) // for desktop we show Row with left content and optional right panel // Expanded( child: isMobile ? AnimatedSwitcher( duration: Duration(milliseconds: 300), transitionBuilder: (child, animation) { final offsetAnimation = Tween( begin: Offset(0, 1), end: Offset(0, 0)) .animate(animation); return SlideTransition( position: offsetAnimation, child: child); }, child: showIRDocs ? _buildMobileFullIrDocs(panelWidth, key: ValueKey('mobile_ir')) : _buildMainLeftContent(key: ValueKey('main_left')), ) : Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // LEFT: main content - scrollable Expanded(child: _buildMainLeftContent()), // RIGHT: IR panel - desktop inline (only visible on wide screens) AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, width: showIRDocs ? panelWidth : 0, child: showIRDocs ? _buildIrDocsPanel(panelWidth, isMobile: false) : const SizedBox.shrink(), ), ], ), ), Expanded( child: isMobile ? AnimatedSwitcher( duration: Duration(milliseconds: 300), transitionBuilder: (child, animation) { final offsetAnimation = Tween( begin: Offset(0, 1), end: Offset(0, 0)) .animate(animation); return SlideTransition( position: offsetAnimation, child: child); }, child: showIRDocs ? buildIRDocsContent( isDesktop: false, key: ValueKey('mobile_ir'), ) : _buildMainLeftContent( key: ValueKey('main_left'), showIRDocs: showIRDocs), ) : Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // LEFT: main content - scrollable Expanded( child: _buildMainLeftContent(showIRDocs: showIRDocs)), // RIGHT: IR panel - desktop inline (only visible on wide screens) AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, width: showIRDocs ? panelWidth : 0, child: showIRDocs ? buildIRDocsContent( isDesktop: true, panelWidth: panelWidth, key: ValueKey('desktop_ir'), ) : SizedBox.shrink(), ), ], ), ), ], ), ), ); } // ------------------------- // Main left content extracted to keep code tidy // ------------------------- Widget _buildMainLeftContent({ Key? key, required bool showIRDocs, }) { return SingleChildScrollView( key: key, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Claim Details card (unchanged) Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: const Color(0xFFEBEBEB), blurRadius: 14, spreadRadius: 2, offset: const Offset(0, 1), ), ], ), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ /// ----------------------------------------- /// NAME + POLICY NAME SECTION /// ----------------------------------------- Responsive.isDesktop(context) ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _buildKeyValue( 'Name', '${widget.empName ?? ''}', ), ), const SizedBox(width: 24), Expanded( child: _buildKeyValue( 'Policy Name', widget.clientPolicyNo == null || widget.clientPolicyNo!.trim().isEmpty ? (widget.policyType ?? '') : '${widget.policyType ?? ''} - ${widget.clientPolicyNo}', ), ), // Expanded( // child: _buildKeyValue( // 'Policy Name', // '${widget.policyType ?? ''} - ${widget.clientPolicyNo ?? ''}', // ), // ), ], ) : SizedBox( width: double.infinity, // ⬅️ forces full width child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildKeyValue( 'Name', '${widget.empName ?? ''}', ), const SizedBox(height: 12), _buildKeyValue( 'Policy Name', '${widget.policyType ?? ''} - ${widget.clientPolicyNo ?? ''}', ), ], ), ), const SizedBox(height: 24), /// ----------------------------------------- /// CLAIM AMOUNT + CLAIM NUMBER SECTION /// ----------------------------------------- Responsive.isDesktop(context) ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Expanded( // child: _buildKeyValue( // 'Claims Amount', // (widget.claimAmount == null || // widget.claimAmount!.trim().isEmpty) // ? 'N/A' // : '₹${widget.claimAmount}', // ), // ), // const SizedBox(width: 24), Expanded( child: _buildKeyValue( 'Claims Number', (widget.claimNo == null || widget.claimNo!.trim().isEmpty) ? 'N/A' : widget.claimNo!, ), ), ], ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // _buildKeyValue( // 'Claims Amount', // (widget.claimAmount == null || // widget.claimAmount!.trim().isEmpty) // ? 'N/A' // : '₹${widget.claimAmount}', // ), // const SizedBox(height: 8), _buildKeyValue( 'Claims Number', (widget.claimNo == null || widget.claimNo!.trim().isEmpty) ? 'N/A' : widget.claimNo!, ), ], ), ], ), ), SizedBox(height: 10), // Loading or step content isLoading ? Container( child: Center( child: Image.asset( height: 60, width: 60, 'assets/nhance-loader.gif'))) : Container( child: getClaimsHistoryList.isNotEmpty ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: useStatusKeyedTicketData ? List.generate(stepKeys.length, (index) { final stepTitleKey = stepKeys[index]; final stepData = Map.from( stepMap[stepTitleKey] ?? {}); return _buildStep( stepNumber: index + 1, title: _getStepTitleFromStatusKey( stepTitleKey, stepData), content: _getStepContentFromApi(stepData), isLast: index == stepKeys.length - 1, ); }) : List.generate(getClaimsHistoryList.length, (index) { final stepData = getClaimsHistoryList[index]; return _buildStep( stepNumber: index + 1, title: _getStepTitleFromApi(stepData), content: _getStepContentFromApi(stepData), isLast: index == getClaimsHistoryList.length - 1, ); }), ) : Container( height: MediaQuery.of(context).size.height * 0.4, child: Center( child: Column( children: [ Image.asset('assets/searchData.jpg', width: 200, height: 200, fit: BoxFit.cover), Text('No Claims History', style: TextStyle( fontWeight: FontWeight.w500, fontSize: 15)), ], ), ), ), ), // Claim files list // Container( // margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), // child: claimFiles.isEmpty // ? const Center(child: Text("No files available")) // : SizedBox( // height: 300, // child: ListView.builder( // itemCount: claimFiles.length, // itemBuilder: (context, index) { // final file = claimFiles[index]; // return Card( // child: ListTile( // leading: const Icon(Icons.insert_drive_file, // color: Colors.blue), // title: Text(file['claim_file_name']), // trailing: // const Icon(Icons.download, color: Colors.green), // onTap: () => _launchURL(file['url']), // ), // ); // }, // ), // ), // ), Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), // 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: claimFiles.isEmpty ? const Center(child: Text("No files available")) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Submitted Documents', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: claimFiles.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: showIRDocs ? 2 : 3, // ✅ desktop: 3 per row crossAxisSpacing: 16, mainAxisSpacing: 16, childAspectRatio: 5.9, // controls height ), itemBuilder: (context, index) { final file = claimFiles[index]; return InkWell( borderRadius: BorderRadius.circular(8), onTap: () => _launchURL(file['url']), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Row( children: [ /// FILE ICON Container( child: const Icon( Icons.insert_drive_file, color: Color(0xFF3B5BDB), size: 25, ), ), const SizedBox(width: 10), /// FILE NAME Expanded( child: Text( file['claim_file_name'] ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, ), ), ), const SizedBox(width: 8), /// DOWNLOAD ICON Tooltip( message: 'Download', // Added tooltip name child: InkWell( borderRadius: BorderRadius.circular(6), onTap: () => _launchURL(file['url']), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: const Color(0xFFE6E6E6), borderRadius: BorderRadius.circular(6), ), child: const Icon( Icons.download, size: 18, color: Colors.black, ), ), ), ), ], ), ), ); }, ), ], ), ) ], ), ); } Widget buildIRDocsContent({ required bool isDesktop, Key? key, double? panelWidth, }) { return Container( key: key ?? ValueKey("ir_docs_unified"), width: isDesktop ? panelWidth ?? 320 : double.infinity, height: double.infinity, padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, border: Border( left: BorderSide( color: Responsive.isDesktop(context) ? Colors.grey.shade300 : Colors.transparent, width: Responsive.isDesktop(context) ? 1 : 0)), ), // color: Colors.white, child: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ----------------------------- // HEADER (same for mobile + web) // ----------------------------- Row( children: [ Expanded( child: Text( "Additional Documents", style: GoogleFonts.poppins( fontSize: isDesktop ? 16 : 18, fontWeight: FontWeight.w600, ), ), ), IconButton( icon: Icon(Icons.close), tooltip: 'Remove', // Built-in property onPressed: () { _resetIRDocs(); setState(() => showIRDocs = false); }, ), ], ), // SizedBox(height: isDesktop ? 8 : 8), // // Text( // "Select documents, click Upload to assign the same file to all selected items.", // style: GoogleFonts.poppins( // fontSize: isDesktop ? 12 : 13, // color: Colors.grey.shade700, // ), // ), SizedBox(height: 12), // ----------------------------- // SCROLLABLE BODY // ----------------------------- Expanded( child: SingleChildScrollView( child: Column( children: [ // Checkbox list (same UI for both) GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, // ✅ two per row crossAxisSpacing: 24, mainAxisSpacing: 8, childAspectRatio: 5, // ✅ controls height (adjust if needed) ), itemCount: requiredDocsList.length, itemBuilder: (context, index) { final d = requiredDocsList[index]; final title = d["document_name"] as String; return Row( children: [ Checkbox( value: d["document_received"] as bool, onChanged: (v) { setState(() { d["document_received"] = v; }); }, ), Expanded( child: Text( title, style: const TextStyle(fontSize: 14), ), ), ], ); }, ), // ...requiredDocsList.map((d) { // final title = d["document_name"] as String; // return CheckboxListTile( // value: d["document_received"] as bool, // onChanged: (v) => // setState(() => d["document_received"] = v), // title: Text(title), // controlAffinity: isDesktop // ? ListTileControlAffinity.leading // : ListTileControlAffinity.trailing, // contentPadding: EdgeInsets.symmetric(horizontal: 8), // ); // }).toList(), SizedBox(height: 12), // Multi uploader (same UI) Padding( padding: const EdgeInsets.all(16.0), child: const MultiFileUploadWidget(forceMobile: true), ), SizedBox(height: 12), // Assigned file preview ..._assignedFiles.entries .where((e) => e.value != null) .map((e) { final title = e.key; final file = e.value!; return Card( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: ListTile( title: Text(title), subtitle: Text( file.name, maxLines: 1, overflow: TextOverflow.ellipsis, ), trailing: IconButton( icon: Icon(Icons.cancel, color: Colors.red), tooltip: 'Remove', // Built-in property onPressed: () => removeAssignedFile(title), ), ), ); }).toList(), SizedBox(height: 16), ], ), ), ), SizedBox(height: 8), // ----------------------------- // SUBMIT BUTTON // ----------------------------- ElevatedButton( onPressed: () => submitIRDocs(), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE26728), minimumSize: Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), ), child: isSubmitting ? SizedBox( height: 22, width: 22, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( 'Submit IR Docs', style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.w500, ), ), ), SizedBox(height: 10), // ----------------------------- // CANCEL BUTTON // ----------------------------- ElevatedButton( onPressed: () { _resetIRDocs(); setState(() => showIRDocs = false); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, minimumSize: Size(double.infinity, 50), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), side: const BorderSide(color: Color(0xFFE26728)), ), ), child: Text( "Cancel", style: GoogleFonts.poppins( color: Color(0xFFE26728), fontWeight: FontWeight.w500, ), ), ), ], ), ), ); } // ------------------------- // Mobile full IR Docs panel (replaces the main content) // ------------------------- Widget _buildMobileFullIrDocs(double panelWidth, {Key? key}) { return Container( key: key ?? ValueKey('mobile_full_ir'), width: double.infinity, height: double.infinity, padding: EdgeInsets.all(12), color: Colors.white, child: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // header row with close (keeps popup header outside) Row( children: [ Expanded( child: Text("IR Docs", style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w600))), IconButton( icon: Icon(Icons.close), tooltip: 'Close', // Built-in property onPressed: () { _resetIRDocs(); setState(() => showIRDocs = false); }, ), ], ), SizedBox(height: 8), Text( "Select documents, click Upload to assign the same file to all selected items.", style: GoogleFonts.poppins( fontSize: 13, color: Colors.grey.shade700)), SizedBox(height: 12), Expanded( child: SingleChildScrollView( child: Column( children: [ ...requiredDocsList.map((d) { final title = d['document_name'] as String; return CheckboxListTile( value: d['document_received'] as bool, onChanged: (v) => setState(() => d['document_received'] = v), title: Text(title), controlAffinity: ListTileControlAffinity.trailing, contentPadding: EdgeInsets.symmetric(horizontal: 8.0), ); }).toList(), SizedBox(height: 12), Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ const MultiFileUploadWidget(forceMobile: true), ], ), ), // Padding( // padding: const EdgeInsets.symmetric(horizontal: 8.0), // child: ElevatedButton.icon( // onPressed: handleUploadForSelected, // icon: Icon(Icons.upload_file), // label: Text("Upload for selected"), // style: ElevatedButton.styleFrom( // backgroundColor: Color(0xFF00A5A8), // minimumSize: Size(double.infinity, 50), // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), // ), // ), // ), SizedBox(height: 12), ..._assignedFiles.entries .where((e) => e.value != null) .map((e) { final title = e.key; final file = e.value!; return Card( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: ListTile( title: Text(title), subtitle: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: IconButton( icon: Icon(Icons.cancel, color: Colors.red), tooltip: 'Remove', // Built-in property onPressed: () => removeAssignedFile(title)), ), ); }).toList(), SizedBox(height: 16), ], ), ), ), Container( alignment: Alignment.center, child: ElevatedButton( onPressed: () { submitIRDocs(); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE26728), // Orange button shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Submit IR Docs', style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.w500, ), ), ], ), ), ), SizedBox(height: 10), Container( alignment: Alignment.center, child: ElevatedButton( onPressed: () { _resetIRDocs(); setState(() => showIRDocs = false); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), side: const BorderSide(color: Color(0xFFE26728)), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Cancel', style: GoogleFonts.poppins(color: const Color(0xFFE26728)), ), ], ), ), ) ], ), ), ); } // ------------------------- // Builds IR Docs panel (desktop) // ------------------------- Widget _buildIrDocsPanel(double panelWidth, {required bool isMobile}) { // The panel has internal scrolling so it won't overflow return Container( width: panelWidth, padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, border: Border(left: BorderSide(color: Colors.grey.shade300, width: 1)), ), child: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // header row Row( children: [ Expanded( child: Text("IR Docs", style: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w600))), IconButton( icon: Icon(Icons.close), tooltip: 'Close', // Built-in property onPressed: () { _resetIRDocs(); setState(() => showIRDocs = false); }, ) ], ), SizedBox(height: 8), Text( "Select documents, click Upload to assign the same file to all selected items.", style: GoogleFonts.poppins( fontSize: 12, color: Colors.grey.shade700)), SizedBox(height: 12), // list + upload + preview inside scroll Expanded( child: SingleChildScrollView( child: Column( children: [ // checkboxes ...requiredDocsList.map((d) { final title = d['document_name'] as String; return CheckboxListTile( value: d['document_received'] as bool, onChanged: (v) => setState(() => d['document_received'] = v), title: Text(title), ); }).toList(), SizedBox(height: 8), // upload button // Padding( // padding: const EdgeInsets.symmetric(horizontal: 4.0), // child: ElevatedButton.icon( // onPressed: handleUploadForSelected, // icon: Icon(Icons.upload_file), // label: Text("Upload for selected"), // style: ElevatedButton.styleFrom( // backgroundColor: Color(0xFF00A5A8), // minimumSize: Size(double.infinity, 44), // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)), // ), // ), // ), Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ const MultiFileUploadWidget(forceMobile: true), ], ), ), SizedBox(height: 12), // assigned files preview ..._assignedFiles.entries .where((e) => e.value != null) .map((e) { final title = e.key; final file = e.value!; return Card( child: ListTile( title: Text(title), subtitle: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: IconButton( icon: Icon(Icons.cancel, color: Colors.red), tooltip: 'Remove', // Built-in property onPressed: () => removeAssignedFile(title)), ), ); }).toList(), SizedBox(height: 16), ], ), ), ), // Submit button (keeps at bottom) Container( alignment: Alignment.center, child: ElevatedButton( onPressed: () { submitIRDocs(); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE26728), // Orange button shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Submit IR Docs', style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.w500, ), ), ], ), ), ), SizedBox(height: 10), Container( alignment: Alignment.center, child: ElevatedButton( onPressed: () { _resetIRDocs(); setState(() => showIRDocs = false); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), side: const BorderSide(color: Color(0xFFE26728)), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Cancel', style: GoogleFonts.poppins(color: const Color(0xFFE26728)), ), ], ), ), ) ], ), ), ); } // ------------------------- // UI Helpers & existing functions below (unchanged) // ------------------------- Widget _buildStep({ required int stepNumber, required Widget title, required Widget content, bool isLast = false, }) { final noContent = (content as Column).children.isEmpty ? 0 : 1; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ SizedBox(height: stepNumber == 1 ? 0 : 4), Container( height: 28, width: 28, decoration: BoxDecoration( color: Color(0xFF00A5A8), shape: BoxShape.circle), alignment: Alignment.center, child: Text( '$stepNumber', style: TextStyle( color: Colors.white, fontSize: Responsive.isDesktop(context) ? 13 : 12, fontWeight: FontWeight.w600, ), ), ), if (!isLast) Container( height: noContent == 1 ? 50 : 20, width: 2, margin: EdgeInsets.only(top: 4, bottom: 4), child: CustomPaint(painter: DottedLinePainter()), ), ], ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ title, if (noContent == 1) SizedBox(height: noContent == 0 ? 0 : 8), if (noContent == 1) Container( width: double.infinity, padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Color(0xFFF7F7F7), borderRadius: BorderRadius.circular(8), border: Border.all(color: Color(0xFFE0E0E0)), ), child: content, ), if (noContent == 1) SizedBox(height: isLast ? 0 : 16), ], ), ), ], ); } Widget _getStepTitleFromStatusKey(String status, Map data) { final modifiedBy = (data['modified_by'] ?? '').toString(); final modifiedAt = (data['modified_at'] ?? data['changed_at'] ?? '').toString(); final symbol = modifiedBy.isNotEmpty ? ' - ' : ''; final isDesktop = Responsive.isDesktop(context); return RichText( text: TextSpan( children: [ TextSpan( text: status + (isDesktop ? ' ' : '\n'), style: GoogleFonts.poppins( fontSize: Responsive.isDesktop(context) ? 15 : 11, fontWeight: FontWeight.w500, color: Color(0xFF212120)), ), TextSpan( text: modifiedAt.isEmpty ? '' : ' ($modifiedBy$symbol$modifiedAt)', style: GoogleFonts.poppins( fontSize: Responsive.isDesktop(context) ? 14 : 11, fontWeight: FontWeight.w400, color: Color(0xFF565656)), ), ], ), ); } Widget _getStepTitleFromApi(Map data) { final status = (data['status'] ?? '').toString(); final changedAt = (data['changed_at'] ?? data['modified_at'] ?? '').toString(); final modifiedBy = (data['modified_by'] ?? '').toString(); final symbol = modifiedBy.isNotEmpty ? ' - ' : ''; final isDesktop = Responsive.isDesktop(context); return RichText( text: TextSpan( children: [ TextSpan( text: status + (isDesktop ? ' ' : '\n'), style: GoogleFonts.poppins( fontSize: Responsive.isDesktop(context) ? 15 : 11, fontWeight: FontWeight.w500, color: Color(0xFF212120)), ), TextSpan( text: changedAt.isEmpty ? '' : ' ($modifiedBy$symbol$changedAt)', style: GoogleFonts.poppins( fontSize: Responsive.isDesktop(context) ? 14 : 11, fontWeight: FontWeight.w400, color: Color(0xFF565656)), ), ], ), ); } Widget _getStepContentFromApi(Map data) { List rows = []; data.forEach((key, value) { if (key == 'modified_by' || key == 'modified_at' || key == 'status' || key == 'changed_at') { return; } if (value is Map) { final displayName = value['display_name']?.toString() ?? key; final displayValue = value['display_value']?.toString() ?? 'N/A'; rows.add(_buildHistoryListData(displayName, displayValue)); rows.add(const SizedBox(height: 1)); return; } if (key == 'reason' && value != null) { final reason = value.toString().trim(); if (reason.isNotEmpty) { rows.add(_buildReasonText(reason)); rows.add(const SizedBox(height: 1)); } } }); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: rows); } // Widget _buildKeyValue(String title, String value) { // final displayValue = // (value == null || value.trim().isEmpty) ? 'N/A' : value; // return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // Text(title, // style: GoogleFonts.poppins( // color: Color(0xFF747474), // fontWeight: FontWeight.w400, // fontSize: Responsive.isDesktop(context) ? 16 : 13)), // SizedBox(height: 4), // Text(displayValue, // style: GoogleFonts.poppins( // color: Color(0xFF000000), // fontWeight: FontWeight.w500, // fontSize: Responsive.isDesktop(context) ? 16 : 13)), // ]); // } Widget _buildKeyValue(String title, String? value) { final displayValue = (value == null || value.trim().isEmpty) ? 'N/A' : value.trim(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: GoogleFonts.poppins( color: const Color(0xFF747474), fontWeight: FontWeight.w400, fontSize: Responsive.isDesktop(context) ? 16 : 13, ), ), const SizedBox(height: 4), /// Prevent overflow everywhere (web & mobile) Text( displayValue, overflow: TextOverflow.ellipsis, maxLines: 1, style: GoogleFonts.poppins( color: const Color(0xFF000000), fontWeight: FontWeight.w500, fontSize: Responsive.isDesktop(context) ? 16 : 13, ), ), ], ); } Widget _buildReasonText(String value) { final displayValue = (value.trim().isEmpty) ? 'N/A' : value.trim(); return Align( alignment: Alignment.centerLeft, child: Text( displayValue, textAlign: TextAlign.left, style: GoogleFonts.poppins( color: const Color(0xFF000000), fontWeight: FontWeight.w400, fontSize: Responsive.isDesktop(context) ? 14 : 12, ), ), ); } Widget _buildHistoryListData(String title, String value) { final displayValue = (value == null || value.trim().isEmpty) ? 'N/A' : value; final titleText = Text(title, style: GoogleFonts.poppins( color: const Color(0xFF747474), fontWeight: FontWeight.w400, fontSize: Responsive.isDesktop(context) ? 14 : 12)); final valueText = Text(displayValue, style: GoogleFonts.poppins( color: const Color(0xFF000000), fontWeight: FontWeight.w400, fontSize: Responsive.isDesktop(context) ? 14 : 12), textAlign: TextAlign.right); return Padding( padding: const EdgeInsets.symmetric(vertical: 1), child: Responsive.isDesktop(context) ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Align( alignment: Alignment.centerLeft, child: titleText)), Expanded( child: Align( alignment: Alignment.centerRight, child: valueText)), ]) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [titleText, SizedBox(height: 4), valueText]), ); } } class DottedLinePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { const dashHeight = 2.0; const dashSpace = 3.0; double startY = 0; final paint = Paint() ..color = Colors.grey.shade400 ..strokeWidth = 1; while (startY < size.height) { canvas.drawLine(Offset(0, startY), Offset(0, startY + dashHeight), paint); startY += dashHeight + dashSpace; } } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }