1134 lines
40 KiB
Dart
1134 lines
40 KiB
Dart
import 'dart:convert';
|
|
// import 'dart:io' as html;
|
|
import 'dart:typed_data'; // Import for Uint8List
|
|
import 'package:dropdown_search/dropdown_search.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:nhance_partner/core/routing/routes.dart';
|
|
import 'package:nhance_partner/presentation/themes/indicators/text_field_theme.dart';
|
|
|
|
import '../../../../core/config/env.dart';
|
|
import '../../../../core/services/api_service.dart';
|
|
import '../../../../data/services/auth_service.dart';
|
|
import '../../../../data/utils/Pagination.dart';
|
|
import '../../../../data/utils/toastNotification.dart';
|
|
import '../../../../data/utils/validators.dart';
|
|
import '../../../layouts/main_layout.dart';
|
|
import '../../../layouts/responsive_layout.dart';
|
|
import '../../../providers/manager_provider.dart';
|
|
import '../../../providers/userRoleProvider.dart';
|
|
import '../../../providers/user_provider.dart';
|
|
import '../../../themes/indicators/customizd_file_upload.dart';
|
|
import '../../../themes/indicators/export_btn.dart';
|
|
import '../../../themes/indicators/input_field_decoration.dart';
|
|
import '../../../themes/indicators/search_field_theme.dart';
|
|
import '../../../themes/indicators/singleFileUpload.dart';
|
|
// import '../../../themes/indicators/upload_doc_theme.dart';
|
|
import 'package:universal_html/html.dart' as html;
|
|
|
|
class Agent extends ConsumerStatefulWidget {
|
|
final String? id;
|
|
final void Function(String value) onSubmit;
|
|
const Agent({super.key, this.id, required this.onSubmit});
|
|
|
|
@override
|
|
ConsumerState<Agent> createState() => AgentState();
|
|
}
|
|
|
|
class AgentState extends ConsumerState<Agent> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late ApiService apiService;
|
|
List<String> tabHeader = [
|
|
'name',
|
|
'email',
|
|
'mobile',
|
|
'code',
|
|
'address',
|
|
'retenRate',
|
|
];
|
|
late String isActive = "1";
|
|
// html.File? docUploadedFile;
|
|
// PlatformFile? passportFile;
|
|
String? selectedFileNames;
|
|
String? passportFileUrlFromApi;
|
|
String? selectedId;
|
|
PlatformFile? docUploadedFile;
|
|
|
|
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKey =
|
|
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
|
|
|
|
Map<String, TextEditingController> controllers = {};
|
|
String? _token;
|
|
dynamic userId;
|
|
dynamic managerId;
|
|
dynamic role;
|
|
|
|
bool isLoading = false;
|
|
dynamic selectedSalesExectv;
|
|
List<Map<String, dynamic>> filteredSalesExecutiveData = [];
|
|
List<Map<String, dynamic>> getSalesExecutiveData = [];
|
|
|
|
Map<String, dynamic> dataDetails() {
|
|
final data = {
|
|
"name": controllers["name"]?.text,
|
|
"email": controllers["email"]?.text,
|
|
"mobile": controllers["mobile"]?.text,
|
|
"address": controllers["address"]?.text,
|
|
"agent_code": controllers["code"]?.text,
|
|
"retention_rate": controllers["retenRate"]?.text,
|
|
'sales_executive_id': selectedSalesExectv,
|
|
"is_active": isActive,
|
|
"manager_id": managerId,
|
|
};
|
|
return data;
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
apiService = ApiService();
|
|
|
|
print('AGID - ${widget.id}');
|
|
|
|
for (String field in tabHeader) {
|
|
controllers[field] = TextEditingController();
|
|
}
|
|
updateData();
|
|
_initializeToken();
|
|
Future.microtask(() {
|
|
managerId = ref.watch(managerIdProvider);
|
|
userId = ref.watch(userIdProvider);
|
|
|
|
role = ref.read(userRoleProvider);
|
|
print("E43 => mId: $managerId");
|
|
if (managerId != null && role != null) {
|
|
getRole(managerId, role);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _initializeToken() async {
|
|
_token = await AuthService.getToken();
|
|
print("APISERTOKEN - $_token");
|
|
}
|
|
|
|
Future<void> getRole(int id, role) async {
|
|
print('getSalesExecutiveData called');
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final response = await apiService.fetchSalesExecutiveList(id, role);
|
|
|
|
if (response['status'] == 'success') {
|
|
print('getRole - ${response['data']}');
|
|
setState(() {
|
|
getSalesExecutiveData = List<Map<String, dynamic>>.from(
|
|
response['data'],
|
|
);
|
|
print('API Data - $getSalesExecutiveData');
|
|
filteredSalesExecutiveData = getSalesExecutiveData
|
|
.where((item) => int.tryParse(item['is_active'].toString()) == 1)
|
|
.toList();
|
|
|
|
print('filteredSalesExecutiveData - $filteredSalesExecutiveData');
|
|
});
|
|
} else {
|
|
getSalesExecutiveData = [];
|
|
filteredSalesExecutiveData = [];
|
|
}
|
|
} catch (e) {
|
|
print('Exception occurred: $e');
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void updateData() async {
|
|
if (widget.id != null && widget.id != 'create') {
|
|
dynamic response = await apiService.findSingleAgentData(widget.id!);
|
|
final data = response['data'];
|
|
print("updateData - ${response['data']}");
|
|
|
|
if (data == null) return;
|
|
setState(() {
|
|
selectedId = widget.id;
|
|
controllers['name']?.text = data['name'] ?? '';
|
|
controllers['email']?.text = data['email'] ?? '';
|
|
controllers['mobile']?.text = data['mobile'] ?? '';
|
|
controllers['code']?.text = data['agent_code'] ?? '';
|
|
controllers['address']?.text = data['address'] ?? '';
|
|
controllers['retenRate']?.text = data['retention_rate'] ?? '';
|
|
isActive = data["is_active"];
|
|
selectedSalesExectv = data['sales_executive_id'] != null
|
|
? int.parse(data['sales_executive_id'].toString())
|
|
: null;
|
|
|
|
print('selectedSalesExectv - $selectedSalesExectv');
|
|
|
|
String? apiDocPath = data["certificate_file_name"];
|
|
if (apiDocPath != null && apiDocPath.isNotEmpty) {
|
|
print('apiDocPath - $apiDocPath');
|
|
selectedFileNames = apiDocPath.split('/').last;
|
|
print('selectedFileNames - $selectedFileNames');
|
|
passportFileUrlFromApi = apiDocPath;
|
|
print('passportFileUrlFromApi - $passportFileUrlFromApi');
|
|
docUploadedFile = null;
|
|
} else {
|
|
selectedFileNames = null;
|
|
docUploadedFile = null;
|
|
passportFileUrlFromApi = null;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> handleSave() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
|
|
final email = controllers['email']!.text.trim();
|
|
final phone = controllers['mobile']!.text.trim();
|
|
|
|
// Conditional validation: at least one required
|
|
if (email.isEmpty && phone.isEmpty) {
|
|
ToastHelper.showSuccessToast(
|
|
context,
|
|
'Please enter either Email or Phone Number',
|
|
);
|
|
return;
|
|
}
|
|
|
|
print("Debug: current selectedSalesExectv is $selectedSalesExectv");
|
|
|
|
setState(() {
|
|
if (_formKey.currentState!.validate()) {
|
|
final dataSet = dataDetails();
|
|
print("dataSetAgent - $dataSet");
|
|
print("managerId - $managerId ,userId - $userId ");
|
|
createUserData(dataSet);
|
|
} else {
|
|
// isDisable = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> createUserData(Map<String, dynamic> userData) async {
|
|
|
|
final String cleanedId = widget.id?.toString().replaceAll(' ', '').toUpperCase() ?? '';
|
|
final bool isUpdating = cleanedId.isNotEmpty && cleanedId != 'CREATE' && cleanedId != '0';
|
|
|
|
final id = widget.id;
|
|
final uri = Uri.parse(
|
|
// isUpdating
|
|
// ? 'https://venbait.in/nhance/partner/dev/api/agent/updateAgent'
|
|
// : 'https://venbait.in/nhance/partner/dev/api/agent/createAgent',
|
|
isUpdating
|
|
? '${Env.apiUrl}agent/updateAgent'
|
|
: '${Env.apiUrl}agent/createAgent',
|
|
);
|
|
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['_method'] = 'PUT';
|
|
request.fields['id'] = id!;
|
|
request.fields['updated_by'] = userId!.toString();
|
|
} else {
|
|
request.fields['created_by'] = userId!.toString();
|
|
}
|
|
|
|
print("USerDAta - $userData");
|
|
|
|
// userData.forEach((key, value) {
|
|
// request.fields[key] = value.toString();
|
|
// print("✅ Encoded travel_details2: ${request.fields[key]}");
|
|
// });
|
|
|
|
userData.forEach((key, value) {
|
|
if (key != 'certificate_file_name') {
|
|
request.fields[key] = value.toString();
|
|
print("✅ Encoded $key: ${request.fields[key]}");
|
|
}
|
|
});
|
|
|
|
// Attach file if selected
|
|
// if (docUploadedFile != null) {
|
|
// try {
|
|
// final reader = html.FileReader();
|
|
// reader.readAsArrayBuffer(docUploadedFile!);
|
|
// await reader.onLoad.first;
|
|
//
|
|
// final data = reader.result as Uint8List;
|
|
//
|
|
// final multipartFile = http.MultipartFile.fromBytes(
|
|
// 'certificate_file_name',
|
|
// data,
|
|
// filename: docUploadedFile!.name,
|
|
// );
|
|
//
|
|
// request.files.add(multipartFile);
|
|
// print("📎 File attached: ${docUploadedFile!.name}");
|
|
// } catch (e) {
|
|
// print("❌ Failed to read file: $e");
|
|
// }
|
|
// } else {
|
|
// print("⚠️ No passport file selected.");
|
|
// }
|
|
|
|
// Attach file if selected
|
|
if (docUploadedFile != null) {
|
|
try {
|
|
if (docUploadedFile!.bytes != null) {
|
|
final multipartFile = http.MultipartFile.fromBytes(
|
|
'certificate_file_name',
|
|
docUploadedFile!.bytes!,
|
|
filename: docUploadedFile!.name,
|
|
);
|
|
request.files.add(multipartFile);
|
|
} else if (docUploadedFile!.path != null) {
|
|
final multipartFile = await http.MultipartFile.fromPath(
|
|
'certificate_file_name',
|
|
docUploadedFile!.path!,
|
|
filename: docUploadedFile!.name,
|
|
);
|
|
request.files.add(multipartFile);
|
|
}
|
|
print("📎 File attached: ${docUploadedFile!.name}");
|
|
} catch (e) {
|
|
print("❌ Failed to attach file: $e");
|
|
}
|
|
}
|
|
|
|
print(" Sending request with fields: ${request.fields}");
|
|
|
|
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) {
|
|
// dispose();
|
|
print("✅ Partner submitted successfully!");
|
|
|
|
print("Response: ${response.body}");
|
|
isUpdating
|
|
? ToastHelper.showSuccessToast(
|
|
context,
|
|
'Partner Updated Successfully',
|
|
)
|
|
: ToastHelper.showSuccessToast(
|
|
context,
|
|
'Partner Created Successfully',
|
|
);
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
widget.onSubmit("success");
|
|
// context.go(AppRoutes.agentLst);
|
|
} else if (response.statusCode == 403) {
|
|
await apiService.clearLocalStorageAndRedirect();
|
|
} else {
|
|
final responseBody = jsonDecode(response.body);
|
|
dynamic msg = responseBody['data'];
|
|
print("❌ Submission failed. Status: ${response.statusCode}");
|
|
print("Body: ${response.body}");
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text("Partner User Creation Failed"),
|
|
content: Text(
|
|
msg,
|
|
// "There was a problem in creating user. Please try again.",
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
child: Text("OK"),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print("🔥 Error submitting user: $e");
|
|
}
|
|
}
|
|
|
|
// Future<void> _downloadFile() async {
|
|
// if (docUploadedFile != null) {
|
|
// try {
|
|
// if (kIsWeb) {
|
|
// // ✅ Web: use bytes
|
|
// final blob = html.Blob([docUploadedFile!.bytes!]);
|
|
// final url = html.Url.createObjectUrlFromBlob(blob);
|
|
//
|
|
// final anchor = html.AnchorElement(href: url)
|
|
// ..setAttribute('download', docUploadedFile!.name)
|
|
// ..click();
|
|
//
|
|
// html.Url.revokeObjectUrl(url);
|
|
// print("Download triggered successfully (web)!");
|
|
// } else {
|
|
// print("Local file path: ${docUploadedFile!.path}");
|
|
// // Use open_filex to open it:
|
|
// // await OpenFilex.open(docUploadedFile!.path!);
|
|
// }
|
|
// } catch (e) {
|
|
// print("Error during download: $e");
|
|
// }
|
|
// } else if (passportFileUrlFromApi != null) {
|
|
// print("Download from API: $passportFileUrlFromApi");
|
|
// final path =
|
|
// 'api/agent/downloadAgentCertificateFile?agent_id=$selectedId';
|
|
//
|
|
// apiService.getPdfDownload(path, selectedId);
|
|
// }
|
|
// }
|
|
|
|
@override
|
|
void dispose() {
|
|
for (var controller in controllers.values) {
|
|
controller.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// managerId = ref.watch(managerIdProvider);
|
|
// userId = ref.watch(userProvider);
|
|
return AlertDialog(
|
|
backgroundColor: Colors.white,
|
|
content: Container(
|
|
// color: Colors.yellow.shade50,
|
|
width: MediaQuery.of(context).size.width * 0.62,
|
|
height: MediaQuery.of(context).size.height * 0.7,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
// height: 30,
|
|
// color: Colors.red.shade50,
|
|
width: MediaQuery.of(context).size.width * 0.8,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
context.go(AppRoutes.agentLst);
|
|
},
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
// Tooltip(
|
|
// message: 'Back',
|
|
// child: IconButton(
|
|
// icon: const Icon(
|
|
// Icons.arrow_left_sharp,
|
|
// size: 25,
|
|
// color: Color(0xFF425B5B),
|
|
// ),
|
|
// onPressed: () {
|
|
// context.go(AppRoutes.agentLst);
|
|
// },
|
|
// splashRadius: 18,
|
|
// hoverColor: Colors.black12,
|
|
// padding: const EdgeInsets.all(4),
|
|
// constraints: const BoxConstraints(),
|
|
// ),
|
|
// ),
|
|
const SizedBox(width: 15), // spacing between icon and text
|
|
Text(
|
|
"Partner",
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
Spacer(),
|
|
GestureDetector(
|
|
onTap: () => Navigator.pop(context),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(5.0),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF1F1F1),
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
),
|
|
child: Tooltip(
|
|
message: 'Close',
|
|
child: const Icon(Icons.close, size: 18),
|
|
),
|
|
),
|
|
),
|
|
// const SizedBox(width: 15),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// SizedBox(height: 10),
|
|
Expanded(
|
|
child: Container(
|
|
// color: Colors.green,
|
|
// width: MediaQuery.of(context).size.width,
|
|
// margin: EdgeInsets.all(8.0),
|
|
padding: EdgeInsets.symmetric(horizontal: 14.0, vertical: 20.0),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
color: Colors.white,
|
|
// color: Color(0xFFEDF6F5),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(children: [buildFormFields()]),
|
|
Spacer(),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () {
|
|
handleSave();
|
|
},
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 45.0,
|
|
vertical: 8,
|
|
),
|
|
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
color: Color(0xFF2E7D6E),
|
|
// color: Color(0xFF425B5B),
|
|
),
|
|
child: Text(
|
|
'Save',
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 30),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
SizedBox(height: 10),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildFormFields() {
|
|
return Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
buildName(),
|
|
SizedBox(width: 25),
|
|
buildEmail(),
|
|
SizedBox(width: 25),
|
|
buildPhNumber(),
|
|
],
|
|
),
|
|
SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
buildId(),
|
|
SizedBox(width: 25),
|
|
buildSalesExecutive(context),
|
|
SizedBox(width: 25),
|
|
buildRetentionRate(context),
|
|
],
|
|
),
|
|
SizedBox(height: 20),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
buildAddress(),
|
|
SizedBox(width: 25),
|
|
buildUploadDocument(),
|
|
SizedBox(width: 25),
|
|
|
|
// Expanded(child: SizedBox.shrink()),
|
|
],
|
|
),
|
|
// Column(
|
|
// children: [
|
|
// Expanded(child: buildIncentiveFile()),
|
|
// SizedBox(width: 25),
|
|
// Expanded(child: SizedBox.shrink()),
|
|
// ],
|
|
// ),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildName() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text("Full Name *", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
ThemedFormField(
|
|
controller: controllers['name']!,
|
|
validator: (value) => Validators.requiredField(value, "name"),
|
|
inputFormatters: [
|
|
// This line now allows letters, numbers, hyphens, and underscores
|
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\-_]')),
|
|
],
|
|
borderColor: Color(0xFFE2E8F0),
|
|
highlightColor: Color(0xFF50A398),
|
|
txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildEmail() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Email *", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
ThemedFormField(
|
|
controller: controllers['email']!,
|
|
validator: (value) {
|
|
final phone = controllers['mobile']!.text.trim();
|
|
|
|
if (value!.isEmpty && phone.isNotEmpty) {
|
|
return null; // phone is given → email not required
|
|
}
|
|
|
|
return Validators.email(value, "email");
|
|
},
|
|
// validator: (value) => Validators.email(value, "email"),
|
|
borderColor: Color(0xFFE2E8F0),
|
|
highlightColor: Color(0xFF50A398),
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_@.]')),
|
|
],
|
|
txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildPhNumber() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Phone Number *", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
ThemedFormField(
|
|
controller: controllers['mobile']!,
|
|
validator: (value) {
|
|
final email = controllers['email']!.text.trim();
|
|
|
|
if (value!.isEmpty && email.isNotEmpty) {
|
|
return null; // email is given → phone not required
|
|
}
|
|
|
|
return Validators.phone(value, "phNumber");
|
|
},
|
|
|
|
// validator: (value) => Validators.phone(value, "phNumber"),
|
|
borderColor: Color(0xFFE2E8F0),
|
|
highlightColor: Color(0xFF50A398),
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
|
|
],
|
|
txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildId() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Partner Id *", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
ThemedFormField(
|
|
controller: controllers['code']!,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
|
|
],
|
|
borderColor: Color(0xFFE2E8F0),
|
|
highlightColor: Color(0xFF50A398),
|
|
validator: (value) => Validators.requiredField(value, "id"),
|
|
txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildRetentionRate(context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Retention Rate*", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
ThemedFormField(
|
|
controller: controllers['retenRate']!,
|
|
inputFormatters: [
|
|
TextInputFormatter.withFunction((oldValue, newValue) {
|
|
if (newValue.text.isEmpty) return newValue;
|
|
|
|
final value = double.tryParse(newValue.text);
|
|
if (value == null) return oldValue;
|
|
|
|
// Allow only values <= 10
|
|
if (value <= 10) {
|
|
return newValue;
|
|
}
|
|
return oldValue;
|
|
}),
|
|
],
|
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
|
borderColor: Color(0xFFE2E8F0),
|
|
highlightColor: Color(0xFF50A398),
|
|
validator: (value) => Validators.requiredField(value, "id"),
|
|
txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildAddress() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Address", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
ThemedFormField(
|
|
controller: controllers['address']!,
|
|
borderColor: Color(0xFFE2E8F0),
|
|
highlightColor: Color(0xFF50A398),
|
|
txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[ a-zA-Z0-9_#/.]')),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildSalesExecutive(BuildContext context) {
|
|
Map<String, dynamic>? selectedSE;
|
|
|
|
if (selectedSalesExectv != null &&
|
|
filteredSalesExecutiveData.isNotEmpty) {
|
|
selectedSE = filteredSalesExecutiveData.firstWhere(
|
|
(item) =>
|
|
item['id'].toString() ==
|
|
selectedSalesExectv.toString(),
|
|
orElse: () => {}, // ✅ MUST be null
|
|
);
|
|
}
|
|
|
|
print('selectedSalesExectv = $selectedSalesExectv');
|
|
|
|
final isReadOnly = false;
|
|
// final isReadOnly = widget.id != null;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Sales Executive *", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
// color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
),
|
|
width: ResponsiveLayout.isMobile(context)
|
|
? null
|
|
: MediaQuery.of(context).size.width * 0.18,
|
|
// height: 40,
|
|
child: AbsorbPointer(
|
|
absorbing: isReadOnly,
|
|
child: DropdownSearch<Map<String, dynamic>>(
|
|
key: dropDownKey,
|
|
selectedItem: selectedSE,
|
|
items: (filter, infiniteScrollProps) {
|
|
return filteredSalesExecutiveData;
|
|
},
|
|
|
|
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;
|
|
// },
|
|
decoratorProps: DropDownDecoratorProps(
|
|
decoration:
|
|
AppInputDecorations.dropdownDecoration(
|
|
label: "Select Executive",
|
|
).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: 1,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(5),
|
|
borderSide: const BorderSide(
|
|
color: Color(0xFFE2E8F0),
|
|
width: 1,
|
|
),
|
|
),
|
|
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,
|
|
decoration: InputDecoration(
|
|
contentPadding: EdgeInsets.all(3),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
hintText: "Search Executive...",
|
|
hintStyle: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
color: Colors.black,
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderSide: BorderSide(
|
|
color: Color(0xFFE2E8F0),
|
|
// color: Colors.white,
|
|
), // 👈 Normal border
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderSide: BorderSide(
|
|
color: Color(0xFFE2E8F0),
|
|
// color: Colors.white,
|
|
width: 1.5,
|
|
), // 👈 Focused border
|
|
),
|
|
enabled: widget.id == null,
|
|
),
|
|
),
|
|
|
|
itemBuilder: (context, item, isDisabled, isSelected) {
|
|
return Container(
|
|
color: isSelected
|
|
? Colors.blue.withOpacity(0.1)
|
|
: Colors.white,
|
|
padding: EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
|
child: Text(
|
|
item['name'].toString(),
|
|
style: GoogleFonts.inter(
|
|
fontSize: 11,
|
|
color: Colors.black87,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
// constraints: BoxConstraints(),
|
|
),
|
|
|
|
onChanged: (val) {
|
|
setState(() {
|
|
selectedSalesExectv = val?['id']?.toString();
|
|
});
|
|
print("UI Updated with ID: $selectedSalesExectv");
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
|
|
Widget buildUploadDocument() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text("Upload Certificate", style: _textStyle),
|
|
SizedBox(height: 10),
|
|
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
ThemedUploadField(
|
|
hintText: selectedFileNames ?? "Upload Document",
|
|
txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
txtheight: 45,
|
|
borderColor: Color(0xFFE2E8F0),
|
|
// highlightColor: Color(0xFF50A398),
|
|
onFileSelected: (fileName, file) {
|
|
print("Picked file: $fileName (${file.size} bytes)");
|
|
setState(() {
|
|
docUploadedFile = file;
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 5),
|
|
|
|
if (docUploadedFile != null || passportFileUrlFromApi != null)
|
|
Container(
|
|
// color: Colors.white,
|
|
child: Column(
|
|
children: [
|
|
// IconButton(
|
|
// onPressed: () {
|
|
// setState(() {
|
|
// docUploadedFile = null;
|
|
// passportFileUrlFromApi = null;
|
|
// selectedFileNames = null;
|
|
// });
|
|
// },
|
|
// icon: Icon(
|
|
// Icons.remove_circle_outline_rounded,
|
|
// color: Colors.redAccent,
|
|
// ),
|
|
// tooltip: 'To Remove Upload',
|
|
// ),
|
|
InkWell(
|
|
onTap: () => apiService.downloadFile(
|
|
apiUrl:
|
|
'api/agent/downloadAgentCertificateFile?agent_id=$selectedId',
|
|
apiId: selectedId,
|
|
localFile: docUploadedFile,
|
|
fileName: selectedFileNames,
|
|
),
|
|
child: Container(
|
|
// padding: const EdgeInsets.all(5),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(5),
|
|
// color: Color(0xFF425B5B),
|
|
// color: Colors.green.shade300,
|
|
),
|
|
child: Column(
|
|
children: const [
|
|
Text(
|
|
"Download",
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
// color: Colors.white,
|
|
color: Color(0xFF2E7D6E),
|
|
),
|
|
),
|
|
// SizedBox(width: 5),
|
|
// Icon(Icons.download, size: 13, color: Colors.white),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// Column(
|
|
// crossAxisAlignment: CrossAxisAlignment.end,
|
|
// children: [
|
|
// ThemedUploadField(
|
|
// hintText: selectedFileNames ?? "Upload Document",
|
|
// txtwidth: MediaQuery.of(context).size.width * 0.18,
|
|
//
|
|
// onFileSelected: (fileName, file) {
|
|
// // print("File picked: ${fileName}");
|
|
// // print("Size: ${file.size}");
|
|
// // print("Path: ${file.path}"); // works on mobile/desktop
|
|
// // print("Bytes: ${file.bytes}");
|
|
// print("Picked file: $fileName (${file.size} bytes)");
|
|
// setState(() {
|
|
// docUploadedFile = file;
|
|
// });
|
|
// },
|
|
// ),
|
|
// SizedBox(height: 5),
|
|
//
|
|
// if (docUploadedFile != null || passportFileUrlFromApi != null)
|
|
// // Centers the text
|
|
// Container(
|
|
// color: Colors.white,
|
|
// child: Column(
|
|
// mainAxisAlignment: MainAxisAlignment.end,
|
|
// crossAxisAlignment: CrossAxisAlignment.end,
|
|
// children: [
|
|
// GestureDetector(
|
|
// onTap: () {
|
|
// print('DOWNLOAD - $docUploadedFile');
|
|
//
|
|
// if (docUploadedFile != null) {
|
|
// try {
|
|
// final blob = html.Blob([docUploadedFile!]);
|
|
//
|
|
// final url = html.Url.createObjectUrlFromBlob(blob);
|
|
//
|
|
// final anchor = html.AnchorElement(href: url)
|
|
// ..setAttribute(
|
|
// 'download',
|
|
// selectedFileNames ?? "document.pdf",
|
|
// )
|
|
// ..click();
|
|
//
|
|
// html.Url.revokeObjectUrl(url);
|
|
//
|
|
// print("Download triggered successfully!");
|
|
// } catch (e) {
|
|
// print("Error during download: $e");
|
|
// }
|
|
// } else if (passportFileUrlFromApi != null) {
|
|
// print("Raw file path: $passportFileUrlFromApi");
|
|
//
|
|
// final path =
|
|
// 'api/agent/downloadAgentCertificateFile?agent_id=$selectedId';
|
|
//
|
|
// apiService.getPdfDownload(path, selectedId);
|
|
// } else {
|
|
// print("No file available to download.");
|
|
// }
|
|
// },
|
|
//
|
|
// // onTap: () {
|
|
// // print('DOWNLOAD - $docUploadedFile');
|
|
// //
|
|
// // if (docUploadedFile != null) {
|
|
// // try {
|
|
// // if (kIsWeb) {
|
|
// // // ✅ WEB: use bytes
|
|
// // final blob = html.Blob([docUploadedFile!.bytes!]);
|
|
// // final url = html.Url.createObjectUrlFromBlob(
|
|
// // blob,
|
|
// // );
|
|
// //
|
|
// // final anchor = html.AnchorElement(href: url)
|
|
// // ..setAttribute(
|
|
// // 'download',
|
|
// // selectedFileNames ?? "document.pdf",
|
|
// // )
|
|
// // ..click();
|
|
// //
|
|
// // html.Url.revokeObjectUrl(url);
|
|
// // print("Download triggered successfully (web)!");
|
|
// // } else {
|
|
// // // ✅ MOBILE/DESKTOP: use file path
|
|
// // print(
|
|
// // "Local file path: ${docUploadedFile!.path}",
|
|
// // );
|
|
// //
|
|
// // // Example: open the file using open_filex
|
|
// // // await OpenFilex.open(docUploadedFile!.path!);
|
|
// // }
|
|
// // } catch (e) {
|
|
// // print("Error during download: $e");
|
|
// // }
|
|
// // } else if (passportFileUrlFromApi != null) {
|
|
// // print("Raw file path: $passportFileUrlFromApi");
|
|
// //
|
|
// // final path =
|
|
// // 'api/agent/downloadAgentCertificateFile?agent_id=$selectedId';
|
|
// //
|
|
// // apiService.getPdfDownload(path, selectedId);
|
|
// // } else {
|
|
// // print("No file available to download.");
|
|
// // }
|
|
// // },
|
|
// child: Container(
|
|
// padding: const EdgeInsets.all(5),
|
|
// decoration: BoxDecoration(
|
|
// borderRadius: BorderRadius.circular(5),
|
|
// color: Colors.green.shade300,
|
|
// ),
|
|
// child: Column(
|
|
// mainAxisAlignment: MainAxisAlignment.end,
|
|
// crossAxisAlignment: CrossAxisAlignment.end,
|
|
// children: [
|
|
// Text(
|
|
// "Download",
|
|
// style: TextStyle(
|
|
// fontSize: 12,
|
|
// fontWeight: FontWeight.w200,
|
|
// color: Colors.white,
|
|
// ),
|
|
// overflow: TextOverflow.ellipsis,
|
|
// textAlign: TextAlign
|
|
// .end, // Ensures text is centered within the Text widget
|
|
// ),
|
|
// SizedBox(width: 5),
|
|
// Icon(Icons.download, size: 13, color: Colors.white),
|
|
// ],
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
],
|
|
);
|
|
}
|
|
|
|
static final _textStyle = GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
);
|
|
}
|