516 lines
16 KiB
Plaintext
516 lines
16 KiB
Plaintext
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");
|
|
}
|
|
}
|
|
|
|
void _showQuotationPopup() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return Dialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Container(
|
|
width: ResponsiveLayout.isMobile(context)
|
|
? MediaQuery.of(context).size.width * 0.9
|
|
: MediaQuery.of(context).size.width * 0.5,
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Quotation Details',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Flexible(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
...filteredData
|
|
.map((item) => _buildQuotationCard(item))
|
|
.toList(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
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
|
|
Widget build(BuildContext context) {
|
|
return 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
|
|
? Column(
|
|
children: [
|
|
// Button to open popup
|
|
ElevatedButton.icon(
|
|
onPressed: _showQuotationPopup,
|
|
icon: const Icon(Icons.visibility),
|
|
label: Text('View Quotations (${filteredData.length})'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF425B5B),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Summary cards
|
|
Expanded(
|
|
child: GridView.count(
|
|
crossAxisCount: ResponsiveLayout.isMobile(context) ? 1 : 3,
|
|
crossAxisSpacing: 16,
|
|
mainAxisSpacing: 16,
|
|
childAspectRatio: ResponsiveLayout.isMobile(context)
|
|
? 3
|
|
: 2,
|
|
children: [
|
|
_buildSummaryCard(
|
|
'Total Quotations',
|
|
filteredData.length.toString(),
|
|
Icons.description,
|
|
Colors.blue,
|
|
),
|
|
_buildSummaryCard(
|
|
'Accepted',
|
|
filteredData
|
|
.where((e) => e['status'] == 'Accepted')
|
|
.length
|
|
.toString(),
|
|
Icons.check_circle,
|
|
Colors.green,
|
|
),
|
|
_buildSummaryCard(
|
|
'Pending',
|
|
filteredData
|
|
.where((e) => e['status'] == 'Pending')
|
|
.length
|
|
.toString(),
|
|
Icons.pending,
|
|
Colors.orange,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: const Center(child: Text('No Available Data')),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryCard(
|
|
String title,
|
|
String count,
|
|
IconData icon,
|
|
Color color,
|
|
) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: color.withOpacity(0.3)),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 32, color: color),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
count,
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: color,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|