import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; import 'package:nhance_app_pwa/pages/postEnrollment/service/api_service.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/service/file_upload_service.dart'; import 'package:nhance_app_pwa/pages/postEnrollment/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/responsive.dart'; import '../../customAppBar/toastHelper.dart'; import '../service/TokenService.dart'; import 'package:nhance_app_pwa/logger.dart'; class ClaimHistoryPopup extends StatefulWidget { final String ticket_id; final String empName; final String empCode; final String policyType; final String? clientPolicyNo; final String? claimAmount; final String? claimNo; final bool isRetailPolicy; const ClaimHistoryPopup({ Key? key, required this.ticket_id, required this.empName, required this.empCode, required this.policyType, this.clientPolicyNo, this.claimAmount, this.claimNo, required this.isRetailPolicy, }) : super(key: key); @override State createState() => _ClaimHistoryPopupState(); } class _ClaimHistoryPopupState 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; // 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.claimAmount); logDebug(widget.claimNo); logDebug(widget.clientPolicyNo); logDebug(widget.empCode); 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'); _token = await TokenService.getPostToken(); // 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.apiUrl}/uploadIRDocs"); final request = http.MultipartRequest('POST', url); request.headers['Authorization'] = "Bearer $_token"; 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"); // 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.getClaimsHistoryToApi(widget.ticket_id); if (response['status'] == 'success') { setState(() { isLoading = false; final claimsDocs = response['data']['claim_files']; claimFiles = List>.from(claimsDocs); getClaimsHistoryList = [ Map.from(response['data']['ticket_data']) ]; stepMap = getClaimsHistoryList[0]; stepKeys = stepMap.keys.toList(); final isFreeze = response['data']['required_docs']['is_action_freeze'] ?? false; setState(() { isActionFreeze = isFreeze; }); final requiredDocs = response['data']['required_docs']['docs']; requiredDocsList = List>.from(requiredDocs); // ⭐ 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 : 320.0; final keyValueWidgets = [ _buildKeyValue( 'Name', '${widget.empName ?? ''} (${widget.empCode ?? ''})'), 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; context.pop(); }, child: Container( // keep popup sized; adjust if required constraints: BoxConstraints(maxWidth: 900, 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: () => context.pop(), 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: 10), // 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')), ) : 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 ? buildIRDocsContent( isDesktop: true, panelWidth: panelWidth, key: ValueKey('desktop_ir'), ) : SizedBox.shrink(), ), ], ), ), ], ), ), ); } // ------------------------- // Main left content extracted to keep code tidy // ------------------------- Widget _buildMainLeftContent({Key? key}) { return Align( alignment: Alignment.topLeft, child: SingleChildScrollView( key: key, padding: EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // Claim Details card (unchanged) if(!widget.isRetailPolicy) Container( margin: EdgeInsets.only( left: 16, right: 16, top: 0, bottom: 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 ?? ''} (${widget.empCode ?? ''})', ), ), 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 ?? ''} (${widget.empCode ?? ''})', ), 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: List.generate(stepKeys.length, (index) { String stepTitleKey = stepKeys[index]; Map stepData = stepMap[stepTitleKey]; Widget content = _getStepContentFromApi(stepData); return _buildStep( stepNumber: index + 1, title: _getStepTitleFromApi(stepTitleKey, stepData), content: content, isLast: index == stepKeys.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 if(!widget.isRetailPolicy) 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']), ), ); }, ), ), ), ], ), )); } 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( "IR Docs", style: GoogleFonts.poppins( fontSize: isDesktop ? 16 : 18, fontWeight: FontWeight.w600, ), ), ), IconButton( icon: Icon(Icons.close), 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) ...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), 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), 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), 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), 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), 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 _getStepTitleFromApi(String status, Map data) { final modifiedBy = data['modified_by'] ?? ''; final modifiedAt = data['modified_at'] ?? ''; final symbol = (data['modified_by'] != null && data['modified_by'] != '') ? ' - ' : ''; 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: ' ($modifiedBy$symbol$modifiedAt)', 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') return; String displayName = value['display_name'] ?? key; String displayValue = value['display_value'] ?? 'N/A'; rows.add(_buildHistoryListData(displayName, displayValue)); rows.add(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 _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; }