staff_policy_upload_pdf

This commit is contained in:
venbaittech 2025-11-01 15:05:30 +05:30
parent bfe7055dc1
commit 872b4e4649
7 changed files with 301 additions and 81 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,6 @@
// import 'dart:io' as html;
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
@ -26,6 +28,7 @@ import '../../../../layouts/main_layout.dart';
import '../../../../layouts/responsive_layout.dart';
import '../../../../providers/manager_provider.dart';
import '../../../../providers/quotation_staff_proivder.dart';
import '../../../../themes/indicators/custom_loader.dart';
import '../../../../themes/indicators/customizd_file_upload.dart';
import '../../../../themes/indicators/date_field_theme.dart';
import '../../../../themes/indicators/text_field_theme.dart';
@ -122,15 +125,20 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
"manager_id": managerId,
// "policy_pdf_file_name": "policy_doc.pdf",
// "policy_payment_receipt_file_name": "receipt_doc.pdf",
"created_by": userId,
// "created_by": userId,
};
return data;
}
Map<String, dynamic> dataForPolicyPDFDetails() {
final data = {"quotation_id": acceptedQuotationId, "manager_id": managerId};
return data;
}
List<Map<String, dynamic>> quotationData = [];
List<Map<String, dynamic>> originalData = [];
List<Map<String, dynamic>> filteredData = [];
// bool isLoading = false;
bool isLoader = false;
@override
void initState() {
@ -336,6 +344,50 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
});
}
String _formatDate(String rawDate) {
try {
final dateTime = DateTime.parse(rawDate);
return DateFormat('dd-MM-yyyy').format(dateTime); // 24-hour format
} catch (e) {
return rawDate; // fallback if parsing fails
}
}
void updateUploadPDFPolicyData(Map<String, dynamic> policyData) {
print('updateUploadPDFPolicyData - $policyData');
if (!mounted || policyData.isEmpty) return;
setState(() {
hasPolicyData = true;
choosedPolcyId = policyData['id']?.toString() ?? '';
controllers["policyNumber"]?.text =
policyData['policy_number']?.toString() ?? '';
controllers["policyPaymentMode"]?.text =
policyData['payment_mode']?.toString() ?? '';
controllers["issueDate"]?.text =
_formatDate(policyData['issued_date']!.toString()) ?? '';
controllers["startDate"]?.text =
_formatDate(policyData['start_date']!.toString()) ?? '';
controllers["endDate"]?.text =
_formatDate(policyData['end_date']!.toString()) ?? '';
// RC FILE
String? rcPath = policyData["policy_pdf_file_name"];
if (rcPath != null && rcPath.isNotEmpty) {
selectedPDFFileNames = rcPath.split('/').last; // UI filename
docUploadedPDFFileUrlFromApi = rcPath; // API download URL
docPDFUploadedFile = null;
// docUploadedRCFile =
// rcFileUrlFromApi as PlatformFile?; // user has not re-uploaded yet
} else {
selectedPDFFileNames = null; // no UI file shown
docUploadedPDFFileUrlFromApi = null;
docPDFUploadedFile = null;
}
});
}
void handleSave() {
if (!_formKey.currentState!.validate()) return;
setState(() {
@ -345,21 +397,12 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
print("dataSetPolicy - $dataSet");
// File validations
if ((docPDFUploadedFile == null || docPDFUploadedFile?.bytes == null) &&
(docUploadedPDFFileUrlFromApi == null ||
docUploadedPDFFileUrlFromApi!.isEmpty)) {
ToastHelper.showErrorToast(context, "Policy PDF is required");
return;
}
// if ((docUploadedFile == null || docUploadedFile?.bytes == null) &&
// (docUploadedFileUrlFromApi == null ||
// docUploadedFileUrlFromApi!.isEmpty)) {
// ToastHelper.showErrorToast(context, "Payment Receipt is required");
// if ((docPDFUploadedFile == null || docPDFUploadedFile?.bytes == null) &&
// (docUploadedPDFFileUrlFromApi == null ||
// docUploadedPDFFileUrlFromApi!.isEmpty)) {
// ToastHelper.showErrorToast(context, "Policy PDF is required");
// return;
// }
// print("managerId - $managerId ,userId - $userId ");
createUserData(dataSet);
} else {
// isDi sable = false;
@ -367,6 +410,22 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
});
}
void handleUploadPolicy() {
setState(() {
dataForPolicyPDFDetails();
final dataSet = dataForPolicyPDFDetails();
print("dataSetPolicy - $dataSet");
// File validations
if ((docPDFUploadedFile == null || docPDFUploadedFile?.bytes == null) &&
(docUploadedPDFFileUrlFromApi == null ||
docUploadedPDFFileUrlFromApi!.isEmpty)) {
ToastHelper.showErrorToast(context, "Policy PDF is required");
return;
}
createPolicyPDFData(dataSet);
});
}
Future<void> attachFiles(http.MultipartRequest request) async {
Future<void> addFileOrKeepName(
PlatformFile? file,
@ -418,13 +477,16 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
Future<void> createUserData(Map<String, dynamic> userData) async {
final bool isUpdating = choosedPolcyId != null;
final id = choosedPolcyId;
// final id = '1';
// print('UPDId- $id');
// final bool isUpdating = false;
final uri = Uri.parse(
isUpdating
? '${Env.apiUrl}policy/updatePolicy'
: '${Env.apiUrl}policy/createPolicy',
'${Env.apiUrl}policy/updatePolicy',
// isUpdating
// ?
// :
// '${Env.apiUrl}policy/createPolicy',
);
if (_token == null) {
throw Exception('Token not found. Please log in.');
@ -517,14 +579,122 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
}
}
Future<void> createPolicyPDFData(Map<String, dynamic> userData) async {
final bool isUpdating = choosedPolcyId != null;
final id = choosedPolcyId;
// final id = '1';
// print('UPDId- $id');
final uri = Uri.parse('${Env.apiUrl}policy/uploadPolicyFile');
if (_token == null) {
throw Exception('Token not found. Please log in.');
}
// Use MultipartRequest (POST only)
final request = http.MultipartRequest('POST', uri);
request.headers['Authorization'] = 'Bearer $_token';
request.headers['app-signature'] = Env.App_Signature;
// If updating, spoof the method Laravel-style
if (isUpdating) {
request.fields['id'] = id!;
request.fields['updated_by'] = userId!.toString();
} else {
request.fields['created_by'] = userId!.toString();
}
print("USerDAta - $userData");
await attachFiles(request);
userData.forEach((key, value) {
if (key != 'policy_pdf_file_name') {
request.fields[key] = value.toString();
print("✅ Encoded $key: ${request.fields[key]}");
} else {
print('Something Missing..');
}
});
print(" Sending request with fields: ${request.fields}");
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const CustomLoader(
message: 'Loading data...',
loaderColor: Colors.deepPurple,
useBlurBackground: true,
),
);
try {
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
print("Response status: ${response.statusCode}");
print("Response body: ${response.body}");
if (response.statusCode == 200 || response.statusCode == 201) {
// Close the loader before showing any message
Navigator.of(context, rootNavigator: true).pop();
// dispose();
print("✅ PDF submitted successfully!");
ToastHelper.showSuccessToast(context, 'PDF Uploaded');
print("Response: ${response.body}");
final Map<String, dynamic> jsonResponse = jsonDecode(response.body);
final policy =
jsonResponse['data']?['file_read_data']?['value']?['policy'];
if (policy != null) {
final extractedData = {
'id': jsonResponse['data']?['policy_id']?.toString(),
'policy_number': policy['policy_number'],
'issued_date': policy['issue_date'],
'start_date': policy['period']?['start'],
'end_date': policy['period']?['end'],
'payment_mode': policy['payment_mode'] ?? 'online',
// optional if available
'policy_pdf_file_name':
jsonResponse['data']?['policy_pdf_file_name'],
// optional if your API returns it
};
print('extractedData - $extractedData');
updateUploadPDFPolicyData(extractedData);
}
} else if (response.statusCode == 403) {
await apiService.clearLocalStorageAndRedirect();
} else {
print("❌ Submission failed. Status: ${response.statusCode}");
print("Body: ${response.body}");
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(" Upload Policy Failed"),
content: Text(
"There was a problem in uploading pdf. Please try again.",
),
actions: [
TextButton(
child: Text("OK"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
} catch (e) {
print("🔥 Error submitting user: $e");
}
}
@override
Widget build(BuildContext context) {
return Container(
// height: MediaQuery.of(context).size.height,
// width: MediaQuery.of(context).size.width,
// padding: const EdgeInsets.all(20),
// child: SingleChildScrollView(
// scrollDirection: Axis.vertical,
child: Column(
children: [
Expanded(
@ -540,17 +710,12 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// SingleChildScrollView(child: buildFormFields(context)),
buildFormFields(context),
// const SizedBox(height: 20),
//confirm button
if (!hasPolicyData && roleId != 'manager') ...[
if (roleId != 'manager') ...[
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
InkWell(
onTap: () {
handleSave();
},
@ -565,7 +730,7 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
color: Color(0xFF425B5B),
),
child: Text(
'Submit',
!hasPolicyData ? 'Submit' : 'Update',
style: TextStyle(
color: Colors.white,
fontSize: 12,
@ -581,46 +746,6 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
),
),
),
// const SizedBox(height: 20),
// Container(
// decoration: BoxDecoration(
// color: const Color(0xFFEDF6F5),
// borderRadius: BorderRadius.circular(10),
// ),
// padding: const EdgeInsets.all(20),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// "Enquiry Details",
// style: GoogleFonts.inter(
// fontSize: 16,
// fontWeight: FontWeight.w700,
// ),
// ),
// SizedBox(height: 15),
// Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Expanded(flex: 2, child: registernumber(context)),
// const SizedBox(width: 20),
// Expanded(flex: 2, child: insurer(context)),
// const SizedBox(width: 20),
// Expanded(flex: 2, child: buildIdv(context)),
// const SizedBox(width: 20),
// Expanded(flex: 2, child: buildPremiumAmnt(context)),
// const SizedBox(width: 20),
// Expanded(flex: 2, child: buildPlanType(context)),
// const SizedBox(width: 20),
// ],
// ),
// ],
// ),
// ),
//
// const SizedBox(height: 20),
],
),
// ),
@ -995,7 +1120,6 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
SizedBox(height: 10),
ThemedFormField(
controller: controllers['premium_amount']!,
readOnly: true,
// validator: (value) =>
// Validators.requiredField(value, "policyPremiumAmount"),
txtwidth: ResponsiveLayout.isMobile(context)
@ -1215,17 +1339,32 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
children: [
ThemedUploadField(
hintText: selectedPDFFileNames ?? "Upload Document",
allowedExtensions: ['pdf'],
txtwidth: ResponsiveLayout.isMobile(context)
? null
: (docUploadedPDFFileUrlFromApi != null)
? MediaQuery.of(context).size.width * 0.15
: MediaQuery.of(context).size.width * 0.175,
// backgroundColor: Color(0xFFEDF6F5),
onFileSelected: (fileName, file) {
onFileSelected: (fileName, file) async {
print("Picked file: $fileName (${file.size} bytes)");
// if (!fileName.toLowerCase().endsWith('.pdf')) {
// ToastHelper.showErrorToast(context, Only PDF files are allowed.)
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Only PDF files are allowed.'),
// backgroundColor: Colors.red,
// ),
// );
// return; // Stop here
// }
print("Picked file: $fileName (${file.size} bytes)");
setState(() {
docPDFUploadedFile = file;
});
handleUploadPolicy();
},
),

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
class CustomLoader extends StatelessWidget {
final String? message;
final Color? backgroundColor;
final double size;
final Color loaderColor;
final bool useBlurBackground;
const CustomLoader({
super.key,
this.message,
this.backgroundColor,
this.size = 50,
this.loaderColor = Colors.blue,
this.useBlurBackground = false,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
if (useBlurBackground) Container(color: Colors.black.withOpacity(0.4)),
Center(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
color: loaderColor,
strokeWidth: 4,
),
),
if (message != null) ...[
const SizedBox(height: 12),
Text(
message!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
],
),
),
),
],
);
}
}

View File

@ -108,6 +108,7 @@ class ThemedUploadField extends StatefulWidget {
final Color? errorBorderColor;
final Function(String fileName, PlatformFile file)? onFileSelected;
FormFieldValidator<String>? validator;
final List<String>? allowedExtensions;
ThemedUploadField({
super.key,
@ -119,6 +120,7 @@ class ThemedUploadField extends StatefulWidget {
this.errorBorderColor,
this.onFileSelected,
this.validator,
this.allowedExtensions,
});
@override
@ -131,7 +133,10 @@ class _ThemedUploadFieldState extends State<ThemedUploadField> {
String? errorMessage;
Future<void> _pickFile() async {
final error = await fileService.pickSingleFile(maxFileSizeInMB: 3);
final error = await fileService.pickSingleFile(
maxFileSizeInMB: 3,
allowedExtensions: widget.allowedExtensions,
);
if (error != null) {
setState(() => errorMessage = error);
ScaffoldMessenger.of(
@ -162,6 +167,7 @@ class _ThemedUploadFieldState extends State<ThemedUploadField> {
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
child: InkWell(
onTap: _pickFile,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(

View File

@ -40,7 +40,8 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
BuildContext context,
FormFieldState<String> field,
) async {
DateTime initialDate = selectedDate ??
DateTime initialDate =
selectedDate ??
(widget.controller?.text.isNotEmpty == true
? DateFormat('dd-MM-yyyy').parse(widget.controller!.text)
: DateTime.now());
@ -78,6 +79,7 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
children: [
InkWell(
onTap: () => pickDate(context, field),
borderRadius: BorderRadius.circular(10),
child: Container(
width: widget.txtwidth ?? MediaQuery.of(context).size.width,
@ -120,6 +122,7 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
),
),
),
if (hasError)
Padding(
padding: const EdgeInsets.only(top: 5, left: 5),

View File

@ -7,14 +7,20 @@ class FileUploadService {
PlatformFile? singleFile;
static const allowedExtensions = ['pdf', 'png', 'jpg', 'jpeg'];
/// Default allowed extensions
static const defaultAllowedExtensions = ['pdf', 'png', 'jpg', 'jpeg'];
// static const allowedExtensions = ['pdf', 'png', 'jpg', 'jpeg'];
Future<String?> pickSingleFile({int maxFileSizeInMB = 5}) async {
Future<String?> pickSingleFile({
int maxFileSizeInMB = 5,
List<String>? allowedExtensions,
}) async {
final extensions = allowedExtensions ?? defaultAllowedExtensions;
final result = await FilePicker.platform.pickFiles(
allowMultiple: false,
withData: true,
type: FileType.custom,
allowedExtensions: allowedExtensions,
allowedExtensions: extensions,
);
if (result != null && result.files.isNotEmpty) {
@ -22,7 +28,7 @@ class FileUploadService {
final ext = file.extension?.toLowerCase() ?? '';
final sizeInMB = file.size / (1024 * 1024);
if (!allowedExtensions.contains(ext)) {
if (!extensions.contains(ext)) {
return "Unsupported format: ${file.name}";
}
if (sizeInMB > maxFileSizeInMB) {

View File

@ -24,6 +24,7 @@ class ThemedFormField extends HookWidget {
this.keyboardType, // new
this.maxLength, // new
this.inputFormatters,
this.isShowCursor,
});
final String? hintText;
@ -38,6 +39,7 @@ class ThemedFormField extends HookWidget {
final Color? errorTextColor;
final double? txtwidth;
final bool readOnly;
final bool? isShowCursor;
final TextInputType? keyboardType;
final int? maxLength;
final double? txtheight;
@ -59,6 +61,7 @@ class ThemedFormField extends HookWidget {
);
final obscureBtn = IconButton(
onPressed: () => isObscured.value = !isObscured.value,
icon: Icon(
isObscured.value
? Icons.visibility_off_outlined
@ -143,10 +146,11 @@ class ThemedFormField extends HookWidget {
readOnly: readOnly, // now supported
keyboardType: keyboardType, // e.g. TextInputType.number
maxLength: maxLength, // max length
showCursor: isShowCursor ?? true,
enableInteractiveSelection: isShowCursor ?? true,
inputFormatters:
inputFormatters ??
[FilteringTextInputFormatter.allow(RegExp(r'[a-z A-Z]'))],
// allow multiline if user sets maxLines / minLines
minLines: (keyboardType == TextInputType.multiline) ? 3 : 1,
maxLines: (keyboardType == TextInputType.multiline) ? null : 1,