enrollment-app/lib/presentation/claimshistory.dart
2026-07-28 10:40:36 +05:30

1766 lines
64 KiB
Dart
Executable File

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 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 String postToken;
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.postToken,
}) : super(key: key);
@override
State<ClaimHistoryPopup> createState() => _ClaimHistoryPopupState();
}
class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
with TickerProviderStateMixin {
late ApiService apiService;
List<Map<String, dynamic>> getClaimsHistoryList = [];
List<String> stepKeys = [];
late Map<String, dynamic> stepMap;
bool isLoading = false;
dynamic _token;
String? selectedFileNames;
// html.File? uploadedFile;
// List<html.File> uploadedFiles = [];
List<PlatformFile> uploadedFiles = [];
final FileUploadService fileService = FileUploadService();
List<dynamic> claimFiles = [];
List<Map<String, dynamic>> requiredDocsList = [];
List<Map<String, dynamic>> requiredDocsListBackup = [];
bool isSubmitting = false;
bool isActionFreeze = false;
bool useStatusKeyedTicketData = false;
bool useListTicketHistory = false;
// IR Docs state
bool showIRDocs = false;
// final List<Map<String, dynamic>> irDocList = [
// {"title": "Hospital Bill", "checked": false},
// {"title": "Discharge Summary", "checked": false},
// {"title": "Prescription", "checked": false},
// ];
final Map<String, PlatformFile?> _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<PlatformFile?> 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<void> 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<void> 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}uploadIRDocs");
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");
// 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<void> getClaimsHistoryDetails() async {
setState(() {
isLoading = true;
});
try {
final response = await apiService.getClaimsHistoryToApi(
widget.ticket_id, widget.postToken);
if (response['status'] == 'success') {
setState(() {
isLoading = false;
final data = response['data'] is Map
? Map<String, dynamic>.from(response['data'])
: <String, dynamic>{};
final claimsDocs = data['claim_files'];
claimFiles = claimsDocs is List
? List<Map<String, dynamic>>.from(claimsDocs)
: <Map<String, dynamic>>[];
_parseTicketData(data);
final requiredDocsRoot = data['required_docs'];
if (requiredDocsRoot is Map) {
isActionFreeze = requiredDocsRoot['is_action_freeze'] ?? false;
final requiredDocs = requiredDocsRoot['docs'];
requiredDocsList = requiredDocs is List
? List<Map<String, dynamic>>.from(requiredDocs)
: <Map<String, dynamic>>[];
} else {
isActionFreeze = false;
requiredDocsList = <Map<String, dynamic>>[];
}
// ⭐ 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');
}
}
void _parseTicketData(Map<String, dynamic> data) {
final ticketDataRaw = data['ticket_data'];
final ticketHistoryRaw = data['ticket_history'];
useStatusKeyedTicketData = false;
useListTicketHistory = false;
stepMap = <String, dynamic>{};
stepKeys = [];
getClaimsHistoryList = [];
if (ticketDataRaw is Map &&
!ticketDataRaw.containsKey('status') &&
ticketDataRaw.values.any((value) => value is Map)) {
useStatusKeyedTicketData = true;
stepMap = Map<String, dynamic>.from(ticketDataRaw);
stepKeys = stepMap.keys.map((key) => key.toString()).toList();
getClaimsHistoryList = [stepMap];
return;
}
if (ticketDataRaw is List && ticketDataRaw.isNotEmpty) {
useListTicketHistory = true;
getClaimsHistoryList = ticketDataRaw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
stepKeys = List.generate(getClaimsHistoryList.length, (index) => '$index');
return;
}
if (ticketHistoryRaw is List && ticketHistoryRaw.isNotEmpty) {
useListTicketHistory = true;
getClaimsHistoryList = ticketHistoryRaw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
stepKeys = List.generate(getClaimsHistoryList.length, (index) => '$index');
return;
}
if (ticketDataRaw is Map && ticketDataRaw.isNotEmpty) {
useListTicketHistory = true;
getClaimsHistoryList = [Map<String, dynamic>.from(ticketDataRaw)];
stepKeys = const ['0'];
}
}
Map<String, dynamic> _asStringMap(dynamic value) {
if (value is Map) {
return Map<String, dynamic>.from(value);
}
return <String, dynamic>{};
}
Future<void> _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 ?? ''} (${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;
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<Offset>( 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<Offset>(
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 ?? ''} (${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) {
if (useListTicketHistory) {
final stepData = getClaimsHistoryList[index];
return _buildStep(
stepNumber: index + 1,
title: _getStepTitleFromHistoryItem(stepData),
content: _getStepContentFromHistoryItem(stepData),
isLast: index == stepKeys.length - 1,
);
}
final stepTitleKey = stepKeys[index];
final stepData = _asStringMap(stepMap[stepTitleKey]);
return _buildStep(
stepNumber: index + 1,
title: _getStepTitleFromApi(
stepTitleKey, stepData),
content: _getStepContentFromApi(stepData),
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
// 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 _getStepTitleFromHistoryItem(Map<String, dynamic> data) {
final title =
(data['display_name'] ?? data['field_name'] ?? 'Update').toString();
final modifiedBy = (data['modified_by'] ?? '').toString();
final modifiedAt =
(data['created_at'] ?? data['modified_at'] ?? '').toString();
final symbol = modifiedBy.isNotEmpty ? ' - ' : '';
final isDesktop = Responsive.isDesktop(context);
return RichText(
text: TextSpan(
children: [
TextSpan(
text: title + (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 _getStepContentFromHistoryItem(Map<String, dynamic> data) {
final oldValue =
(data['old_status_value'] ?? data['old_value'] ?? '-').toString();
final newValue =
(data['new_status_value'] ?? data['new_value'] ?? '-').toString();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHistoryListData('Previous Value', oldValue),
const SizedBox(height: 1),
_buildHistoryListData('Updated Value', newValue),
],
);
}
Widget _getStepTitleFromApi(String status, Map<String, dynamic> 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<String, dynamic> data) {
final rows = <Widget>[];
for (final entry in data.entries) {
final key = entry.key.toString();
final value = entry.value;
if (key == 'modified_by' || key == 'modified_at') continue;
if (value is Map) {
final nested = Map<String, dynamic>.from(value);
final displayName =
(nested['display_name'] ?? key).toString();
final displayValue =
(nested['display_value'] ?? 'N/A').toString();
rows
..add(_buildHistoryListData(displayName, displayValue))
..add(const SizedBox(height: 1));
continue;
}
if (key == 'reason') {
final reason = value?.toString().trim() ?? '';
if (reason.isNotEmpty) {
rows
..add(_buildReasonText(reason))
..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;
}