1766 lines
64 KiB
Dart
Executable File
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;
|
|
}
|