584 lines
18 KiB
Dart
584 lines
18 KiB
Dart
import 'dart:convert';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
|
|
import '../../../../../core/config/env.dart';
|
|
import '../../../../../core/services/api_service.dart';
|
|
import '../../../../../data/services/auth_service.dart';
|
|
import '../../../../../data/utils/toastNotification.dart';
|
|
import '../../../layouts/responsive_layout.dart';
|
|
import '../../../providers/manager_provider.dart';
|
|
|
|
class QuotationPopUpTab extends ConsumerStatefulWidget {
|
|
String? id;
|
|
final Future<void> Function()? onRefresh;
|
|
QuotationPopUpTab({super.key, this.id, this.onRefresh});
|
|
|
|
@override
|
|
ConsumerState<QuotationPopUpTab> createState() => QuotationTabState();
|
|
}
|
|
|
|
class QuotationTabState extends ConsumerState<QuotationPopUpTab> {
|
|
late ApiService apiService;
|
|
|
|
bool isLoading = false;
|
|
bool blockKey = false;
|
|
|
|
String? _token;
|
|
dynamic userId;
|
|
dynamic managerId;
|
|
dynamic role;
|
|
|
|
List<Map<String, dynamic>> getQuotationData = [];
|
|
List<Map<String, dynamic>> originalData = [];
|
|
List<Map<String, dynamic>> filteredData = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
apiService = ApiService();
|
|
_initializeToken();
|
|
Future.microtask(() {
|
|
managerId = ref.watch(managerIdProvider);
|
|
userId = ref.watch(userIdProvider);
|
|
role = ref.watch(userRoleProvider);
|
|
getQuotationList();
|
|
});
|
|
}
|
|
|
|
Future<void> _initializeToken() async {
|
|
_token = await AuthService.getToken();
|
|
print("APISERTOKEN - $_token");
|
|
}
|
|
|
|
void refresh() {
|
|
getQuotationList();
|
|
}
|
|
|
|
/// ✅ Instead of using `widget.data`, call the API directly
|
|
Future<void> getQuotationList() async {
|
|
print('Fetching quotation list from API...');
|
|
setState(() => isLoading = true);
|
|
|
|
try {
|
|
if (widget.id == null) {
|
|
throw Exception('QuotationTab: ID is null');
|
|
}
|
|
|
|
// Example: adjust this endpoint based on your actual API
|
|
final response = await apiService.findEnqQuotePolicyView(widget.id);
|
|
|
|
// Extract quotation data safely
|
|
final quotations = (response["data"]?["quotations"] as List?) ?? [];
|
|
|
|
final data = quotations
|
|
.map((e) => Map<String, dynamic>.from(e as Map))
|
|
.toList();
|
|
|
|
setState(() {
|
|
getQuotationData = data;
|
|
filteredData = List.from(data);
|
|
blockKey = data.any((item) => item['status'] == 'Accepted');
|
|
});
|
|
|
|
print('Fetched quotation data: ${data.length}');
|
|
} catch (e, s) {
|
|
print('❌ Error fetching quotations: $e\n$s');
|
|
} finally {
|
|
setState(() => isLoading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> handleAction(String action, String quotationId) async {
|
|
final String apiUrldata = '${Env.apiUrl}quotation/acceptOrRejectQuotation';
|
|
|
|
if (_token == null) {
|
|
throw Exception('Token not found. Please log in.');
|
|
}
|
|
|
|
final Map<String, dynamic> data = {
|
|
"id": quotationId,
|
|
"status": action,
|
|
"action_by": userId,
|
|
"action_user": role,
|
|
};
|
|
|
|
print("data------- $data");
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse(apiUrldata),
|
|
headers: {
|
|
'Authorization': 'Bearer $_token',
|
|
'Content-Type': 'application/json',
|
|
'app-signature': Env.App_Signature,
|
|
},
|
|
body: jsonEncode(data),
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
print("Response: ${response.body}");
|
|
ToastHelper.showSuccessToast(context, 'Status Updated');
|
|
|
|
// Close popup
|
|
Navigator.of(context).pop();
|
|
|
|
// Refresh parent
|
|
if (widget.onRefresh != null) {
|
|
await widget.onRefresh!();
|
|
}
|
|
} else if (response.statusCode == 403) {
|
|
await apiService.clearLocalStorageAndRedirect();
|
|
} else {
|
|
final responseBody = jsonDecode(response.body);
|
|
ToastHelper.showErrorToast(context, 'Status Updation Failed');
|
|
print("Failed to submit. Status: ${response.statusCode}");
|
|
print("Error: ${response.body}");
|
|
}
|
|
} catch (e) {
|
|
print("Error submitting: $e");
|
|
}
|
|
}
|
|
|
|
Widget _buildQuotationPopupContent() {
|
|
return Container(
|
|
// width: ResponsiveLayout.isMobile(context)
|
|
// ? MediaQuery.of(context).size.width * 0.9
|
|
// : MediaQuery.of(context).size.width * 0.5,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
// border: Border.all(color: const Color(0xFFD9EBE8)),
|
|
// boxShadow: [
|
|
// BoxShadow(
|
|
// color: Colors.black.withOpacity(0.05),
|
|
// blurRadius: 8,
|
|
// offset: const Offset(0, 4),
|
|
// ),
|
|
// ],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Proposal Details',
|
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
|
),
|
|
|
|
IconButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
icon: Icon(Icons.close),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 5),
|
|
Flexible(
|
|
child: SingleChildScrollView(
|
|
child: _buildQuotationTable(), // ✅ Only one table
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuotationTable() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFFD9EBE8)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 🔹 Table Header
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
|
color: const Color(0xFFEFF6F5),
|
|
child: Row(
|
|
children: const [
|
|
Expanded(
|
|
flex: 1,
|
|
child: Text(
|
|
"IDV",
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
"Plan Type",
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 1,
|
|
child: Text(
|
|
"Premium",
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 3,
|
|
child: Center(
|
|
child: Text(
|
|
"Action",
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1, color: Color(0xFFD9EBE8)),
|
|
|
|
// 🔹 Table Rows
|
|
...filteredData.map((item) {
|
|
final bool isAccepted = item['status'] == 'Accepted';
|
|
final bool isRejected = item['status'] == 'Rejected';
|
|
final bool isPending = item['status'] == 'Pending';
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.grey.withOpacity(0.2)),
|
|
),
|
|
color: isAccepted
|
|
? Colors.green.withOpacity(0.05)
|
|
: isRejected
|
|
? Colors.red.withOpacity(0.05)
|
|
: Colors.white,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
child: Text(
|
|
item['insured_declared_value']?.toString() ?? '-',
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
item['insurance_plan_type']?.toString() ?? '-',
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 1,
|
|
child: Text(
|
|
item['premium_amount']?.toString() ?? '-',
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 3,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
if (isPending && !blockKey) ...[
|
|
TextButton(
|
|
onPressed: () =>
|
|
handleAction('Rejected', item['id']),
|
|
style: TextButton.styleFrom(
|
|
backgroundColor: Colors.red.shade50,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Reject',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: () =>
|
|
handleAction('Accepted', item['id']),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF425B5B),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Accept',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
if (isAccepted)
|
|
const Text(
|
|
'Accepted',
|
|
style: TextStyle(
|
|
color: Colors.green,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
if (isRejected)
|
|
const Text(
|
|
'Rejected',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuotationCard(Map<String, dynamic> item) {
|
|
final bool isAccepted = item['status'] == 'Accepted';
|
|
final bool isRejected = item['status'] == 'Rejected';
|
|
final bool isPending = item['status'] == 'Pending';
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isAccepted
|
|
? Colors.green.withOpacity(0.1)
|
|
: isRejected
|
|
? Colors.red.withOpacity(0.1)
|
|
: const Color(0xFFF6FEFD),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: isAccepted
|
|
? Colors.green
|
|
: isRejected
|
|
? Colors.red
|
|
: const Color(0xffD9EBE8),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header with status
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
item['insurer_name'] ?? '-',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: _getStatusColor(item['status']),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
item['status'] ?? '-',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Details
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildDetailItem(
|
|
'IDV',
|
|
item['insured_declared_value']?.toString() ?? '-',
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _buildDetailItem(
|
|
'Premium',
|
|
item['premium_amount']?.toString() ?? '-',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildDetailItem(
|
|
'Plan Type',
|
|
item['insurance_plan_type']?.toString() ?? '-',
|
|
),
|
|
|
|
// Action buttons (only if pending and no other quotation is accepted)
|
|
if (isPending && !blockKey) ...[
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => handleAction('Rejected', item['id']),
|
|
style: TextButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
backgroundColor: Colors.red.shade50,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Reject',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
ElevatedButton(
|
|
onPressed: () => handleAction('Accepted', item['id']),
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
backgroundColor: const Color(0xFF425B5B),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Accept',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
|
|
// Show message if blocked
|
|
if (isPending && blockKey) ...[
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.info_outline, size: 16, color: Colors.orange),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Another quotation has been accepted',
|
|
style: TextStyle(fontSize: 12, color: Colors.orange),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailItem(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'$label: ',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF545454),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _getStatusColor(String? status) {
|
|
switch (status) {
|
|
case 'Pending':
|
|
return Colors.orange;
|
|
case 'Accepted':
|
|
return Colors.green;
|
|
case 'Rejected':
|
|
return Colors.red;
|
|
default:
|
|
return Colors.grey;
|
|
}
|
|
}
|
|
|
|
@override
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SelectionArea(
|
|
child: Container(
|
|
// height: MediaQuery.of(context).size.height,
|
|
// width: MediaQuery.of(context).size.width,
|
|
// padding: const EdgeInsets.all(16),
|
|
child: isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: filteredData.isNotEmpty
|
|
? SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
_buildQuotationPopupContent(), // ✅ now renders inline
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
)
|
|
: const Center(child: Text('No Available Data')),
|
|
),);
|
|
}
|
|
}
|