multi_file_upload

This commit is contained in:
venbaittech 2025-11-27 09:40:08 +05:30
parent 6a78b1c6d2
commit 53a86a8e21
11 changed files with 737 additions and 116 deletions

File diff suppressed because one or more lines are too long

View File

@ -363,6 +363,31 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
const SizedBox(width: 16),
],
// Proposal button
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200, width: 1),
),
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.request_quote_outlined,
color: Colors.black87,
size: 20,
),
onPressed: () {
debugPrint("Notifications tapped");
},
tooltip: 'Create Proposal',
),
),
const SizedBox(width: 12),
// Notifications button
Container(
width: 36,
@ -432,7 +457,7 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
),
),
Text(
(role == 'agent') ? 'partner' : role ?? 'manager',
(role == 'agent') ? 'Partner' : role ?? 'Manager',
style: GoogleFonts.inter(
fontSize: 9,
color: Colors.grey.shade600,

View File

@ -556,12 +556,14 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
ThemedSearchField(
hintText: 'Search',
backgroundColor: Color(0xFFF6F8F8),
backgroundColor: Color(0xFFFFFFFF),
// backgroundColor: Color(0xFFF6F8F8),
onChanged: filterData,
controller: _searchStaffController,
txtwidth: ResponsiveLayout.isMobile(context)
? MediaQuery.of(context).size.width * 0.6
: MediaQuery.of(context).size.width * 0.13,
txtHeight: 30,
),
ResponsiveLayout.isMobile(context)
? Spacer()
@ -614,26 +616,30 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
// print('Export');
},
child: Container(
padding: EdgeInsets.all(8.0),
padding: EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: Color(0xFF425B5B),
color: const Color(0xFF2E7D6E),
// color: Color(0xFF425B5B),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, color: Colors.white),
Tooltip(
message: 'Raise Enquiry',
child: Icon(Icons.add, color: Colors.white, size: 18),
),
if (!ResponsiveLayout.isMobile(context)) ...[
SizedBox(width: 10),
Text(
'Raise Enquiry',
style: GoogleFonts.inter(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
// SizedBox(width: 10),
// Text(
// 'Raise Enquiry',
// style: GoogleFonts.inter(
// color: Colors.white,
// fontWeight: FontWeight.w600,
// fontSize: 14,
// ),
// ),
// GestureDetector(
// onTap: () {
// ref
@ -666,10 +672,11 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
if (!ResponsiveLayout.isMobile(context))
Container(
decoration: BoxDecoration(
color: Color(0xFFEDF6F5),
// color: Color(0xFFEDF6F5),
color: Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [
Expanded(flex: 1, child: Text('S.No.', style: _headerStyle)),
@ -773,10 +780,11 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
Widget _buildDataRow(Map<String, dynamic> item, sno) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 16),
// margin: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: Colors.white,
// color: Color(0xFFE0F7F9),
border: const Border(
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
@ -869,13 +877,13 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
child: IconButton(
icon: Image.asset(
"assets/miscellaneous/Edit.png",
height: 15,
height: 12,
width: 15,
),
onPressed: () {
handleEdit(item);
},
splashRadius: 28,
splashRadius: 18,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
@ -1152,7 +1160,7 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
}
static final _dataBold = GoogleFonts.inter(
fontSize: 14,
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xFF000000),
@ -1168,9 +1176,15 @@ class EnquiryListState extends ConsumerState<EnquiryList> {
// color: Colors.black,
// fontWeight: FontWeight.bold,
// );
static final _headerStyle = GoogleFonts.inter(
color: Colors.black,
fontWeight: FontWeight.bold,
// static final _headerStyle = GoogleFonts.inter(
// color: Colors.black,
// fontWeight: FontWeight.bold,
// );
static final _headerStyle = GoogleFonts.poppins(
fontSize: 11.2,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
);
static final _cardheaderStyle = GoogleFonts.inter(
color: Colors.black,

View File

@ -1013,8 +1013,9 @@ class agentPendings extends StatelessWidget {
print('-Data: $data');
return Card(
color: const Color(0xFFEAF6F4),
elevation: 0,
color: Colors.white,
// color: const Color(0xFFEAF6F4),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(12.0),
@ -1029,7 +1030,8 @@ class agentPendings extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xFF3E5B56),
color: Color(0xFFE3F9F8),
// color: const Color(0xFF3E5B56),
borderRadius: BorderRadius.circular(10),
),
child: Row(
@ -1122,14 +1124,17 @@ class agentPendings extends StatelessWidget {
onStaffEdit!(row['id'].toString(), stringFlag);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
// margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.symmetric(
vertical: 5,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(2),
border: Border(
bottom: BorderSide(color: Colors.blueGrey.shade50),
),
),
child: Row(
children: [

View File

@ -12,6 +12,7 @@ import 'package:intl/intl.dart';
import 'package:nhance_partner/presentation/screens/staff/Enquiry/quotationPopUpAccptReject.dart';
import 'package:nhance_partner/presentation/screens/staff/Enquiry/raise_enq_form.dart';
import 'package:nhance_partner/presentation/screens/staff/Enquiry/tabs/tab.dart';
import 'package:nhance_partner/presentation/themes/indicators/customizd_multi_file_upload.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/config/env.dart';
@ -132,8 +133,9 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
"email": controllers["email"]?.text,
"reg_no": controllers["regNo"]?.text,
"vehicle_type_id": selectedVehicleType,
"is_data_created_by_handler": roleId == 'handler' ? '1' : '0',
"is_data_created_by_manager": roleId == 'manager' ? '1' : '0',
// "is_data_created_by_handler": roleId == 'handler' ? '1' : '0',
// "is_data_created_by_manager": roleId == 'manager' ? '1' : '0',
"is_data_created_by_staff": roleId != null ? '1' : '0',
// "insurer_id": selectedInsurer,
// "rc_file_name": "rc_doc.pdf",
// "id_proof_file_name": "id_proof.pdf",
@ -1706,7 +1708,7 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
}) {
final isMobile = ResponsiveLayout.isMobile(context);
final uploadWidget = ThemedUploadField(
final uploadWidget = ThemedMultiFileUploadField(
hintText: hintText ?? "Upload Document",
txtwidth: width,
// txtwidth: MediaQuery.of(context).size.width * 0.08,
@ -1720,6 +1722,20 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
onFileSelected: onFileSelected,
);
// final uploadWidget = ThemedUploadField(
// hintText: hintText ?? "Upload Document",
// txtwidth: width,
// // txtwidth: MediaQuery.of(context).size.width * 0.08,
// padVertical: 6,
// padHorizontal: 8,
//
// backgroundColor: Colors.white,
// // backgroundColor: Color(0xffD9EBE8),
// txtName: label,
// isTxtBtnCase: true,
// onFileSelected: onFileSelected,
// );
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
@ -2120,9 +2136,8 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
spacing: 12,
runSpacing: 12,
children: [
buildAgentName(context, fromHeader: false),
buildSelectStaffMem(context),
buildAgentName(context, fromHeader: false),
buildInsurer(context),
buildInsuredName(context),
buildVehicleNumber(context),
@ -3135,25 +3150,25 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
return DataRow(
cells: [
// DataCell(
// SizedBox(
// width: 80,
// child: Row(
// children: [
// // Text(item['created_on'], style: _dataBold),
// Text(
// formatDateTimeForTable(item['created_on'], newLine: true),
// style: _dataBoldthm1,
// ),
// ],
// ),
// ),
// ),
DataCell(
SizedBox(
width: 80,
child: Row(
children: [
// Text(item['created_on'], style: _dataBold),
Text(
formatDateTimeForTable(item['created_on'], newLine: true),
style: _dataBoldthm1,
),
],
),
),
),
// Assigned To
DataCell(
SizedBox(
// flex: 1,
width: 100,
width: 120,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
@ -3164,7 +3179,7 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
: '-',
style: _dataBold,
),
// Text(_formatDate(item['updated_on']), style: _dataBoldthm4),
Text(_formatDate(item['updated_on']), style: _dataBoldthm4),
],
),
),
@ -3522,7 +3537,7 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
// Add this helper method to generate DataColumns
List<DataColumn> _buildDataColumns() {
return [
// DataColumn(label: Text('Received Date', style: _headerStyle)),
DataColumn(label: Text('Received Date', style: _headerStyle)),
DataColumn(label: Text('Assigned To *', style: _headerStyle)),
DataColumn(label: Text('Partner *', style: _headerStyle)),
@ -3545,14 +3560,14 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
// _buildHeaderCell('Received Date', 120),
_buildHeaderCell('Received Date', 110),
_buildHeaderCell('Assigned To *', 130),
_buildHeaderCell('Partner *', 160),
// _buildHeaderCell('Broker *', 100),
_buildHeaderCell('Insurer *', 90),
_buildHeaderCell('Insured Name *', 120),
_buildHeaderCell('Vehicle No *', 110),
_buildHeaderCell('Insured Name *', 110),
_buildHeaderCell('Vehicle No *', 90),
_buildHeaderCell('Action', 60),
// _buildHeaderCell('Assigned Date', 100),
_buildHeaderCell('Premium', 100),

View File

@ -106,8 +106,9 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
"email": controllers["email"]?.text,
"reg_no": controllers["regNo"]?.text,
"vehicle_type_id": selectedVehicleType,
"is_data_created_by_handler": roleId == 'handler' ? '1' : '0',
"is_data_created_by_manager": roleId == 'manager' ? '1' : '0',
// "is_data_created_by_handler": roleId == 'handler' ? '1' : '0',
// "is_data_created_by_manager": roleId == 'manager' ? '1' : '0',
"is_data_created_by_staff": roleId != null ? '1' : '0',
// "insurer_id": selectedInsurer,
// "rc_file_name": "rc_doc.pdf",
// "id_proof_file_name": "id_proof.pdf",
@ -164,6 +165,7 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
print('hansles');
getStaffDetailsForEnquiryAssignment(userID);
}
getBroker();
});
getVehicleType();
@ -256,6 +258,37 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
}
}
Future<void> getBroker() async {
print('getBroker called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchMasterDropDown('Broker');
if (response['status'] == 200) {
print('getBroker - ${response['data']}');
setState(() {
getBrokerData = List<Map<String, dynamic>>.from(response['data']);
print('API Data - $getBrokerData');
filteredBrokerData = List.from(getBrokerData);
print('originalData - $filteredBrokerData');
});
} else {
getBrokerData = [];
filteredBrokerData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> getAgentList(id) async {
print('getAgentListData called');
setState(() {
@ -356,9 +389,11 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
children: [
buildAgentName(context, fromHeader: true),
if (roleId != 'staff') ...[buildSelectStaffMem(context)],
buildInsurer(context, fromHeader: true),
buildInsuredName(context, fromHeader: true),
buildBroker(context),
buildInsurer(context, fromHeader: true),
// buildInsuredName(context, fromHeader: true),
buildVehicleNumber(context),
Row(
children: [
@ -931,6 +966,155 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
);
}
Widget buildBroker(BuildContext context) {
Map<String, dynamic>? selectedBrokers = filteredBrokerData.firstWhere(
(item) => item['id'].toString() == selectedBroker,
orElse: () => {},
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Broker *', style: _subLabelTimeStyle),
SizedBox(height: 5),
SizedBox(
// height: 30,
width: MediaQuery.of(context).size.width * 0.12,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKeyBroker,
selectedItem: selectedBrokers.isNotEmpty ? selectedBrokers : null,
items: (filter, infiniteScrollProps) {
return filteredBrokerData;
},
itemAsString: (val) => val['name'].toString(), // what to show
compareFn: (item, selectedItem) =>
item['id'] == selectedItem['id'], // compare by id
validator: (val) {
if (val == null) {
return "Required"; // error message
}
return null;
},
suffixProps: DropdownSuffixProps(
// make sure the dropdown button is visible
dropdownButtonProps: DropdownButtonProps(
isVisible: true,
padding: EdgeInsets.zero, // remove default padding
constraints: const BoxConstraints(
// shrink icon tap area
minWidth: 12,
minHeight: 12,
),
iconSize: 15, // smaller icon
// icon: const Icon(Icons.arrow_drop_down),
),
),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem != null ? selectedItem['name'].toString() : "",
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
),
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Select Broker",
).copyWith(
filled: true,
fillColor:
Colors.white, // 👈 makes the dropdown input white
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
),
),
popupProps: PopupProps.menu(
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
menuProps: MenuProps(
backgroundColor:
Colors.white, // 👈 sets dropdown background to white
),
showSearchBox: true,
searchFieldProps: TextFieldProps(
autofocus: true,
style: GoogleFonts.inter(fontSize: 11, color: Colors.black),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(1),
filled: true,
fillColor: Colors.white,
hintText: "Search Broker...",
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
), // 👈 Normal border
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
width: 1.5,
), // 👈 Focused border
),
),
),
itemBuilder: (context, item, isDisabled, isSelected) {
return Container(
// color: isSelected ? Colors.blue.withOpacity(0.1) : null,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
child: Text(
item['name'].toString(),
style: GoogleFonts.inter(fontSize: 12, color: Colors.black),
),
);
},
// constraints: BoxConstraints(),
),
onChanged: (val) {
if (val != null) {
print("Selected Broker : ${val['name']}");
print("Id: ${val['id']}");
selectedBroker = val['id'];
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
}
},
),
),
],
);
}
Widget buildSave(context) {
return InkWell(
onTap: () {

View File

@ -504,37 +504,37 @@ class CreateProposalFormState extends ConsumerState<CreateProposalForm> {
child: buildFormFields(context),
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () {
handleDone();
// widget.onSubmit(controller.text.trim());
// Navigator.of(context).pop();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 25.0,
vertical: 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: const Color(0xFF425B5B),
),
child: Text(
'Save',
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
// Row(
// crossAxisAlignment: CrossAxisAlignment.end,
// mainAxisAlignment: MainAxisAlignment.end,
// children: [
// GestureDetector(
// onTap: () {
// handleDone();
// // widget.onSubmit(controller.text.trim());
// // Navigator.of(context).pop();
// },
// child: Container(
// padding: const EdgeInsets.symmetric(
// horizontal: 25.0,
// vertical: 10,
// ),
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(8.0),
// color: const Color(0xFF425B5B),
// ),
// child: Text(
// 'Save',
// style: GoogleFonts.poppins(
// color: Colors.white,
// fontSize: 12,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// ),
// ],
// ),
],
);
}
@ -555,6 +555,7 @@ class CreateProposalFormState extends ConsumerState<CreateProposalForm> {
Spacer(),
buildPremiumAmnt(context),
Spacer(),
// buildDocuments(context),
// if (!ResponsiveLayout.isMobile(context)) const SizedBox(height: 10),
@ -579,6 +580,38 @@ class CreateProposalFormState extends ConsumerState<CreateProposalForm> {
buildDocuments(context), Spacer(),
],
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () {
handleDone();
// widget.onSubmit(controller.text.trim());
// Navigator.of(context).pop();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 25.0,
vertical: 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: const Color(0xFF425B5B),
),
child: Text(
'Save',
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
),
);

View File

@ -897,33 +897,33 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
),
SizedBox(height: 1),
// if (roleId != 'manager') ...[
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
onTap: () {
handleSave();
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 45.0, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: Color(0xFF425B5B),
),
child: Text(
// !hasPolicyData ? 'Submit' : 'Update',
'Submit',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
// Row(
// mainAxisAlignment: MainAxisAlignment.end,
// children: [
// InkWell(
// onTap: () {
// handleSave();
// },
// child: Container(
// padding: EdgeInsets.symmetric(horizontal: 45.0, vertical: 8),
//
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(8.0),
// color: Color(0xFF425B5B),
// ),
// child: Text(
// // !hasPolicyData ? 'Submit' : 'Update',
// 'Submit',
// style: TextStyle(
// color: Colors.white,
// fontSize: 12,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// ),
// ],
// ),
// ],
// ),
],
@ -1160,6 +1160,37 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
onTap: () {
handleSave();
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 45.0,
vertical: 8,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
color: Color(0xFF425B5B),
),
child: Text(
// !hasPolicyData ? 'Submit' : 'Update',
'Submit',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
],
],

View File

@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:google_fonts/google_fonts.dart';
import 'fileUploadService.dart';
import 'multi_fileUploadService.dart';
class ThemedMultiFileUploadField extends StatefulWidget {
final String? hintText;
final double? txtwidth;
final double? txtheight;
final Color? backgroundColor;
final Color? borderColor;
final Color? errorBorderColor;
final Function(String fileName, PlatformFile file)? onFileSelected;
FormFieldValidator<String>? validator;
final List<String>? allowedExtensions;
final bool isTxtBtnCase;
final String? txtName;
final double? padVertical;
final double? padHorizontal;
ThemedMultiFileUploadField({
super.key,
this.hintText,
this.txtwidth,
this.txtheight,
this.backgroundColor,
this.borderColor,
this.errorBorderColor,
this.onFileSelected,
this.validator,
this.allowedExtensions,
this.isTxtBtnCase = false,
this.txtName,
this.padVertical,
this.padHorizontal,
});
@override
State<ThemedMultiFileUploadField> createState() =>
_ThemedMultiFileUploadFieldState();
}
class _ThemedMultiFileUploadFieldState
extends State<ThemedMultiFileUploadField> {
final fileService = MultiFileUploadService();
String? selectedFileName;
String? errorMessage;
Future<void> _pickFile() async {
final error = await fileService.pickFiles(
maxFileSizeInMB: 3,
allowedExtensions: widget.allowedExtensions,
);
if (error != null) {
setState(() => errorMessage = error);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error)));
} else if (fileService.pickFiles != null) {
// setState(() {
// selectedFileName = fileService.pickFiles!.name;
// errorMessage = null;
// });
// widget.onFileSelected?.call(
// fileService.pickFiles!.name,
// fileService.pickFiles!,
// );
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: widget.txtwidth ?? MediaQuery.of(context).size.width,
height: widget.txtheight,
child: DecoratedBox(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
child: InkWell(
onTap: _pickFile,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: EdgeInsets.symmetric(
vertical: widget.padVertical ?? 10,
horizontal: widget.padHorizontal ?? 15,
),
decoration: BoxDecoration(
color: widget.backgroundColor ?? Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: errorMessage != null
? (widget.errorBorderColor ?? Colors.red)
: (widget.borderColor ??
widget.backgroundColor ??
Colors.grey.shade50),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (!widget.isTxtBtnCase) ...[
Expanded(
child: Text(
selectedFileName ??
widget.hintText ??
"Upload Document",
style: TextStyle(
fontSize: 12,
color: errorMessage != null
? Colors.red
: Colors.black,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(
Icons.file_upload_outlined,
color: Colors.black,
),
],
if (widget.isTxtBtnCase) ...[
Center(
child: Tooltip(
message: 'Upload Documents',
child: Text(
widget.txtName!,
style: GoogleFonts.inter(fontSize: 12),
),
),
),
// const Icon(
// Icons.file_upload_outlined,
// color: Colors.black,
// ),
],
],
),
),
),
),
),
if (errorMessage != null)
Padding(
padding: const EdgeInsets.only(left: 8, top: 4),
child: Text(
errorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 10),
),
),
],
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:file_picker/file_picker.dart';
class MultiFileUploadService {
MultiFileUploadService._();
static final MultiFileUploadService _instance = MultiFileUploadService._();
factory MultiFileUploadService() => _instance;
final List<PlatformFile> _files = [];
List<PlatformFile> get files => List.unmodifiable(_files);
// Allowed extensions
// static const allowedExtensions = ['pdf', 'png', 'jpg', 'jpeg', 'heic'];
static const defaultAllowedExtensions = ['pdf', 'png', 'jpg', 'jpeg'];
Future<String?> pickFiles({
int maxFileSizeInMB = 3,
List<String>? allowedExtensions,
}) async {
final extensions = allowedExtensions ?? defaultAllowedExtensions;
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
withData: true,
type: FileType.custom,
allowedExtensions: extensions,
);
if (result != null) {
for (final file in result.files) {
final ext = file.extension?.toLowerCase() ?? '';
final sizeInMB = (file.size / (1024 * 1024));
if (!allowedExtensions!.contains(ext)) {
return "Unsupported file format: ${file.name}";
}
if (sizeInMB > maxFileSizeInMB) {
return "File too large (${file.name}). Max $maxFileSizeInMB MB allowed.";
}
}
_files.addAll(result.files);
}
return null; // success
}
void removeFileAt(int index) {
if (index >= 0 && index < _files.length) _files.removeAt(index);
}
void clearAll() => _files.clear();
}

View File

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'multi_fileUploadService.dart';
class MultiFileUploadWidget extends StatefulWidget {
const MultiFileUploadWidget({super.key});
@override
State<MultiFileUploadWidget> createState() => _MultiFileUploadWidgetState();
}
class _MultiFileUploadWidgetState extends State<MultiFileUploadWidget> {
final fileService = MultiFileUploadService();
String? errorMessage;
void _pickFiles() async {
final error = await fileService.pickFiles(
maxFileSizeInMB: 10,
); // 5 MB limit
if (error != null) {
if (mounted) {
setState(() {
errorMessage = error;
});
// also show alert dialog for big error messages
// showDialog(
// context: context,
// builder: (ctx) => AlertDialog(
// title: const Text("File Upload Error"),
// content: Text(error),
// actions: [
// TextButton(
// onPressed: () => Navigator.pop(ctx),
// child: const Text("OK"),
// ),
// ],
// ),
// );
}
} else {
setState(() {
errorMessage = null;
});
}
}
void _removeFile(int index) {
fileService.removeFileAt(index);
setState(() {});
}
@override
Widget build(BuildContext context) {
final files = fileService.files;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
OutlinedButton.icon(
onPressed: _pickFiles,
icon: const Icon(
Icons.file_upload_outlined,
color: Color(0xFFE26728),
size: 24,
),
label: const Text(
"Upload Documents",
style: TextStyle(fontSize: 14, color: Colors.black),
overflow: TextOverflow.ellipsis,
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFE26728)),
),
),
if (errorMessage != null) ...[
const SizedBox(height: 4),
Text(
errorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
],
const SizedBox(height: 8),
...files.asMap().entries.map((entry) {
final index = entry.key;
final file = entry.value;
return ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: Text(file.name, style: const TextStyle(fontSize: 14)),
trailing: IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => _removeFile(index),
),
);
}),
],
);
}
}