sales executive

This commit is contained in:
venbaittech 2025-12-10 09:43:46 +05:30
parent 0054bfeb30
commit 3477cd99f3
19 changed files with 2648 additions and 245 deletions

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,8 @@ import '../../presentation/screens/StaffAttendance/individual_Attendance.dart';
import '../../presentation/screens/UserManagement/Agent/agent.dart';
import '../../presentation/screens/UserManagement/Agent/agentList.dart';
import '../../presentation/screens/UserManagement/Profile/profile_mobile.dart';
import '../../presentation/screens/UserManagement/SalesExecutive/salesExecutive.dart';
import '../../presentation/screens/UserManagement/SalesExecutive/salesExecutiveList.dart';
import '../../presentation/screens/UserManagement/Staff/staff.dart';
import '../../presentation/screens/UserManagement/Staff/staffList.dart';
import '../../presentation/screens/dashboard/dashboard.dart';
@ -114,6 +116,24 @@ final GoRouter appRouter = GoRouter(
}
},
),
GoRoute(
path: AppRoutes.salesExecutiveLst,
// builder: (context, state) => const Agent(),
builder: (context, state) => const SalesExecutiveList(),
),
GoRoute(
path: '/salesExecutive/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
// if "create", open empty form
if (id == 'create') {
return const SalesExecutive(); // create mode
} else {
return SalesExecutive(id: id); // update mode
}
},
),
GoRoute(
path: AppRoutes.enquiryLst,

View File

@ -8,6 +8,8 @@ class AppRoutes {
static const agent = '/agent/:id';
static const String staffLst = '/staffLst';
static const staff = '/staff/:id';
static const String salesExecutiveLst = '/salesExecutiveLst';
static const salesExecutive = '/salesExecutive/:id';
static const String enquiryLst = '/enquiryLst';
static const String enquiryForStaff = '/enquiryForStaff';
static const String tabEnquiry = '/tabEnquiry';

View File

@ -428,7 +428,9 @@ class ApiService {
};
final url;
if (role == 'staff') {
if (role == 'salesExecutive') {
url = Uri.parse('${Env.apiUrl}salesExecutive/changeExecutiveStatus');
} else if (role == 'staff') {
url = Uri.parse('${Env.apiUrl}staff/changeStaffStatus');
} else {
url = Uri.parse('${Env.apiUrl}/api/agent/changeAgentStatus');
@ -557,6 +559,23 @@ class ApiService {
return response;
}
Future<Map<String, dynamic>> findProductivityDashboardData(id, broker) async {
// print(_token);
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse(
'${Env.apiUrl}dashboard/productivityDashboard?manager_id=$id',
);
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
// -------------------------------- AGENT ----------------------------------------------
Future<Map<String, dynamic>> fetchAgentIncentiveList(agentId) async {
@ -705,6 +724,57 @@ class ApiService {
final response = await _makeGetRequest(url, headers);
return response;
}
// ------------------------------------ SALES EXECUTIVE -----------------------------------------------------
Future<Map<String, dynamic>> fetchSalesExecutiveList(
int managerId,
role,
) async {
// print(_token);
print('rolemanagerId - $managerId');
if (_token == null) {
await _initializeToken();
}
dynamic url;
if (role == 'manager') {
print('manager');
// url = Uri.parse('${Env.apiUrl}staff/staffList');
url = Uri.parse(
'${Env.apiUrl}salesExecutive/executiveList?manager_id=${managerId}',
);
// url = Uri.parse('${Env.apiUrl}salesExecutive/executiveList');
} else {
print('handler');
url = Uri.parse(
'${Env.apiUrl}salesExecutive/executiveList?handler_id=${managerId}',
);
}
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
Future<Map<String, dynamic>> findSingleSalesExecutiveData(id) async {
// print(_token);
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}salesExecutive/findExecutive?id=$id');
// final url = Uri.parse('${Env.apiUrl}staff/findStaff?id=$id');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
final response = await _makeGetRequest(url, headers);
return response;
}
// ----------------------------------- ENQUIRY -------------------------------------------------

View File

@ -56,7 +56,8 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
} else if (currentRoute.contains('enquiry')) {
_activeMenu = 'Enquiry';
} else if (currentRoute.contains('agent') ||
currentRoute.contains('staff')) {
currentRoute.contains('staff') ||
currentRoute.contains('salesExecutive')) {
_activeMenu = 'User';
} else if (currentRoute.contains('claim') ||
currentRoute.contains('endosement') ||
@ -145,6 +146,15 @@ class _AppHeaderState extends ConsumerState<AppHeader> {
context.go(AppRoutes.staffLst);
},
),
const SizedBox(height: 2),
_buildPopupItem(
label: "Sales Executive",
onTap: () {
_hidePopup();
setState(() => _activeMenu = 'User');
context.go(AppRoutes.salesExecutiveLst);
},
),
],
if (key == 'Reports') ...[
if (role == 'manager') ...[

View File

@ -481,8 +481,11 @@ class _AddDialogState extends State<AddDialog> {
onChanged: filterData,
controller: _searchStaffController,
txtwidth: ResponsiveLayout.isMobile(context)
? MediaQuery.of(context).size.width * 0.7
: MediaQuery.of(context).size.width * 0.15,
? null
: MediaQuery.of(context).size.width * 0.26,
// txtwidth: ResponsiveLayout.isMobile(context)
// ? MediaQuery.of(context).size.width * 0.7
// : MediaQuery.of(context).size.width * 0.15,
),
const SizedBox(height: 16),

View File

@ -1,6 +1,7 @@
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';
@ -20,10 +21,13 @@ 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';
@ -48,10 +52,19 @@ class AgentState extends ConsumerState<Agent> {
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;
String? selectedSalesExectv;
List<Map<String, dynamic>> filteredSalesExecutiveData = [];
List<Map<String, dynamic>> getSalesExecutiveData = [];
Map<String, dynamic> dataDetails() {
final data = {
@ -60,6 +73,7 @@ class AgentState extends ConsumerState<Agent> {
"mobile": controllers["mobile"]?.text,
"address": controllers["address"]?.text,
"agent_code": controllers["code"]?.text,
'sales_executive_id': selectedSalesExectv,
"is_active": isActive,
"manager_id": managerId,
};
@ -79,6 +93,12 @@ class AgentState extends ConsumerState<Agent> {
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);
}
});
}
@ -87,6 +107,41 @@ class AgentState extends ConsumerState<Agent> {
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!);
@ -102,6 +157,7 @@ class AgentState extends ConsumerState<Agent> {
controllers['code']?.text = data['agent_code'] ?? '';
controllers['address']?.text = data['address'] ?? '';
isActive = data["is_active"];
selectedSalesExectv = data['sales_executive_id'] ?? '';
String? apiDocPath = data["certificate_file_name"];
if (apiDocPath != null && apiDocPath.isNotEmpty) {
@ -470,12 +526,19 @@ class AgentState extends ConsumerState<Agent> {
SizedBox(height: 20),
Row(
children: [
Expanded(child: buildAddress()),
Expanded(child: buildSalesExecutive(context)),
SizedBox(width: 25),
Expanded(child: buildUploadDocument()),
Expanded(child: buildAddress()),
],
),
SizedBox(height: 20),
Row(
children: [
Expanded(child: buildUploadDocument()),
SizedBox(width: 25),
Expanded(child: SizedBox.shrink()),
],
),
// Row(
// children: [
// Expanded(child: buildIncentiveFile()),
@ -595,6 +658,157 @@ class AgentState extends ConsumerState<Agent> {
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
txtwidth: MediaQuery.of(context).size.width * 0.26,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[ a-zA-Z0-9_#/.]')),
],
),
],
);
}
Widget buildSalesExecutive(BuildContext context) {
Map<String, dynamic>? selectedroleVal = filteredSalesExecutiveData
.firstWhere(
(item) => item['id'].toString() == selectedSalesExectv,
orElse: () => {},
);
final isReadOnly = widget.id != null;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Sales Executive *", style: _textStyle),
SizedBox(width: 10),
Container(
decoration: BoxDecoration(
// color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.26,
// height: 40,
child: AbsorbPointer(
absorbing: isReadOnly,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKey,
selectedItem: selectedroleVal.isNotEmpty ? selectedroleVal : null,
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: widget.id != null
? null
: (val) {
if (val != null) {
print("Selected Role : ${val['role']}");
print("Id: ${val['id']}");
selectedSalesExectv = val['id'];
}
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
},
),
),
),
],
);

View File

@ -0,0 +1,928 @@
import 'dart:convert';
// import 'dart:io' as html;
import 'dart:typed_data'; // Import for Uint8List
import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/cupertino.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/data/utils/toastNotification.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/validators.dart';
import '../../../layouts/main_layout.dart';
import '../../../layouts/responsive_layout.dart';
import '../../../providers/manager_provider.dart';
import 'package:universal_html/html.dart' as html;
import '../../../themes/indicators/input_field_decoration.dart';
class SalesExecutive extends ConsumerStatefulWidget {
final String? id;
const SalesExecutive({super.key, this.id});
@override
ConsumerState<SalesExecutive> createState() => SalesExecutiveState();
}
class SalesExecutiveState extends ConsumerState<SalesExecutive> {
final _formKey = GlobalKey<FormState>();
late ApiService apiService;
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKey =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>>
dropDownKeyHandler = GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
List<String> tabHeader = ['name', 'email', 'mobile', 'code', 'address'];
late String isActive = "1";
html.File? passportFile;
String? selectedFileNames;
String? passportFileUrlFromApi;
String? selectedId;
String? _token;
bool isLoading = false;
bool showHandler = false;
String? selectedRole;
List<Map<String, dynamic>> filteredRolesData = [];
List<Map<String, dynamic>> getRolesData = [];
List<dynamic>? selectedHandlerIds = [];
String? selectedHandler;
List<Map<String, dynamic>> filteredHandlersData = [];
List<Map<String, dynamic>> getHandlersData = [];
Map<String, TextEditingController> controllers = {};
dynamic userId;
dynamic managerId;
Map<String, dynamic> dataDetails() {
final data = {
"name": controllers["name"]?.text,
"email": controllers["email"]?.text,
"mobile": controllers["mobile"]?.text,
"address": controllers["address"]?.text,
// "emp_id": controllers["code"]?.text,
"is_active": isActive,
// "handler_id": selectedHandler,
"manager_id": userId,
};
return data;
}
@override
void initState() {
super.initState();
apiService = ApiService();
for (String field in tabHeader) {
controllers[field] = TextEditingController();
}
_initializeToken();
updateData();
getRole();
Future.microtask(() {
managerId = ref.watch(managerIdProvider);
userId = ref.watch(userIdProvider);
if (managerId != null) {
print('hansles');
getHandlers(managerId);
}
});
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
print("APISERTOKEN - $_token");
}
void updateData() async {
if (widget.id != null && widget.id != 'create') {
dynamic response = await apiService.findSingleSalesExecutiveData(
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['address']?.text = data['address'] ?? '';
// controllers['code']?.text = data['emp_id'] ?? '';
isActive = data["is_active"];
});
}
}
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;
}
setState(() {
if (_formKey.currentState!.validate()) {
dataDetails();
final dataSet = dataDetails();
print("dataSetAgent - $dataSet");
createUserData(dataSet);
} else {
// isDisable = false;
}
});
}
Future<void> createUserData(data) async {
final bool isUpdating = widget.id != null && widget.id != 'create';
final String? id = isUpdating ? widget.id : null;
final String apiUrldata;
apiUrldata = isUpdating
? '${Env.apiUrl}salesExecutive/updateExecutive'
: '${Env.apiUrl}salesExecutive/createExecutive';
// final token = await getToken(); // Fetch token
if (_token == null) {
throw Exception('Token not found. Please log in.');
}
if (isUpdating) {
data['id'] = id; // Add plan_id for update
data['updated_by'] = userId;
} else {
data['created_by'] = userId;
}
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), // Convert map to JSON
);
if (response.statusCode == 200) {
print("SalesExecutive submitted successfully!");
print("Response: ${response.body}");
isUpdating
? ToastHelper.showSuccessToast(
context,
'SalesExecutive Updated Successfully',
)
: ToastHelper.showSuccessToast(
context,
'SalesExecutive Created Successfully',
);
context.go(AppRoutes.salesExecutiveLst);
} else if (response.statusCode == 403) {
await apiService.clearLocalStorageAndRedirect();
} else {
final responseBody = jsonDecode(response.body);
dynamic msg = responseBody['data'];
print("Failed to submit plan. Status: ${response.statusCode}");
print("Error: ${response.body}");
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("SalesExecutive User Creation Failed"),
content: Text(msg),
actions: [
TextButton(
child: Text("OK"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
} catch (e) {
print(" Error submitting SalesExecutive: $e");
}
}
Future<void> getRole() async {
print('Handlers called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchMasterDropDown('staffRole');
if (response['status'] == 200) {
print('getRole - ${response['data']}');
setState(() {
getRolesData = List<Map<String, dynamic>>.from(response['data']);
print('API Data - $getRolesData');
filteredRolesData = List.from(getRolesData);
print('originalData - $filteredRolesData');
});
} else {
getRolesData = [];
filteredRolesData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> getHandlers(managerId) async {
print('Handlers called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchHandlerNameDropDown(managerId);
if (response['code'] == 200 || response['status'] == 'success') {
print('getHandlers -');
print('getHandlers - ${response['data']}');
setState(() {
getHandlersData = List<Map<String, dynamic>>.from(response['data']);
print('API HandlersData - $getHandlersData');
filteredHandlersData = List.from(getHandlersData);
print('HandlersoriginalData - $filteredHandlersData');
});
} else {
getHandlersData = [];
filteredHandlersData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
@override
void dispose() {
for (var controller in controllers.values) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return MainLayout(
title: "SalesExecutive",
body: Container(
// color: Colors.yellow.shade50,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
// height: 30,
// color: Colors.red.shade50,
width: MediaQuery.of(context).size.width,
child: GestureDetector(
onTap: () {
context.go(AppRoutes.staffLst);
},
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.salesExecutiveLst);
},
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
),
const SizedBox(width: 5), // spacing between icon and text
Text(
"Sales Executive",
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
// 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: [Expanded(child: 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(
children: [
Row(
children: [
Expanded(child: buildName()),
SizedBox(width: 25),
Expanded(child: buildEmail()),
],
),
SizedBox(height: 20),
Row(
children: [
Expanded(child: buildPhNumber()),
SizedBox(width: 25),
Expanded(child: buildAddress()),
],
),
SizedBox(height: 20),
],
),
);
}
Widget buildName() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Name *", style: _textStyle),
SizedBox(width: 10),
ThemedFormField(
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
controller: controllers['name']!,
validator: (value) => Validators.requiredField(value, "name"),
txtwidth: MediaQuery.of(context).size.width * 0.26,
),
],
);
}
Widget buildEmail() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Email *", style: _textStyle),
SizedBox(width: 10),
ThemedFormField(
controller: controllers['email']!,
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
// validator: (value) => Validators.email(value, "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");
},
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_@.]')),
],
txtwidth: MediaQuery.of(context).size.width * 0.26,
),
],
);
}
Widget buildPhNumber() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Phone Number *", style: _textStyle),
SizedBox(width: 10),
ThemedFormField(
controller: controllers['mobile']!,
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
// validator: (value) => Validators.phone(value, "phNumber"),
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");
},
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
],
txtwidth: MediaQuery.of(context).size.width * 0.26,
),
],
);
}
Widget buildAddress() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Address", style: _textStyle),
SizedBox(width: 10),
ThemedFormField(
controller: controllers['address']!,
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[ a-zA-Z0-9_#/.]')),
],
txtwidth: MediaQuery.of(context).size.width * 0.26,
),
],
);
}
Widget buildId() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("SalesExecutive Id ", style: _textStyle),
SizedBox(width: 10),
ThemedFormField(
controller: controllers['code']!,
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
// validator: (value) => Validators.requiredField(value, "id"),
txtwidth: MediaQuery.of(context).size.width * 0.26,
),
],
);
}
// Widget buildAddress() {
// return Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text("Role*", style: _textStyle),
// SizedBox(width: 10),
// ThemedFormField(
// controller: controllers['address']!,
//
// txtwidth: MediaQuery.of(context).size.width * 0.26,
// ),
// ],
// );
// }
Widget buildRole(BuildContext context) {
Map<String, dynamic>? selectedroleVal = filteredRolesData.firstWhere(
(item) => item['id'].toString() == selectedRole,
orElse: () => {},
);
if (selectedroleVal.isNotEmpty &&
selectedroleVal['role'] != null &&
selectedroleVal['role'] == 'SalesExecutive') {
showHandler = true;
} else {
selectedHandlerIds = [];
showHandler = false;
}
final isReadOnly = widget.id != null;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Role *", style: _textStyle),
SizedBox(width: 10),
Container(
decoration: BoxDecoration(
// color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.26,
// height: 40,
child: AbsorbPointer(
absorbing: isReadOnly,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKey,
selectedItem: selectedroleVal.isNotEmpty ? selectedroleVal : null,
items: (filter, infiniteScrollProps) {
return filteredRolesData;
},
itemAsString: (val) => val['role'].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 Role",
).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 Role...",
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,
),
),
// constraints: BoxConstraints(),
),
onChanged: widget.id != null
? null
: (val) {
if (val != null) {
print("Selected Role : ${val['role']}");
print("Id: ${val['id']}");
selectedRole = val['id'];
if (val['role'] == 'SalesExecutive') {
setState(() {
showHandler = true;
});
} else {
setState(() {
showHandler = false;
});
}
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
}
},
),
),
),
],
);
}
Widget buildHandler(BuildContext context) {
Map<String, dynamic>? selectedHandlered = filteredHandlersData.firstWhere(
(item) => item['id'].toString() == selectedHandler,
orElse: () => {},
);
// final isReadOnly = widget.id != null;
final isReadOnly = false;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Handler Name *", style: _textStyle),
SizedBox(width: 10),
Container(
decoration: BoxDecoration(
// color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
width: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.26,
// height: 40,
child: AbsorbPointer(
absorbing: isReadOnly,
// child: DropdownSearch<Map<String, dynamic>>(
child: DropdownSearch<Map<String, dynamic>>.multiSelection(
key: dropDownKeyHandler,
// selectedItem: selectedHandlered.isNotEmpty
// ? selectedHandlered
// : null,
selectedItems: filteredHandlersData
.where(
(item) => (selectedHandlerIds ?? []).contains(item['id']),
)
.toList(),
items: (filter, infiniteScrollProps) {
return filteredHandlersData;
},
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;
// },
validator: (val) {
if (val == null || val.isEmpty) {
return "Required";
}
return null;
},
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Select Handler",
).copyWith(
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
filled: true,
fillColor:
Colors.white, // 👈 makes the dropdown input white
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
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
),
),
popupProps: PopupPropsMultiSelection.menu(
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
showSearchBox: true,
menuProps: MenuProps(backgroundColor: Colors.white),
searchFieldProps: TextFieldProps(
autofocus: true,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: "Search Handler...",
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
// color: Colors.blue,
color: Color(0xFFEDF6F5),
width: 1.5,
),
),
),
),
// checkBoxBuilder: (context, item, isSelected, isDisabled) {
// return Icon(
// isSelected
// ? Icons.check_box
// : Icons.check_box_outline_blank,
// color: isSelected ? Color(0xFFE26728) : Colors.red,
// );
// },
// validationBuilder: (context, selectedItems) {
// return Padding(
// padding: const EdgeInsets.symmetric(
// vertical: 8.0,
// horizontal: 8.0,
// ),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.end,
// children: [
// ElevatedButton(
// style: ElevatedButton.styleFrom(
// backgroundColor: Color(0xFF425B5B),
// foregroundColor: Colors.white,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(8),
// ),
// minimumSize: Size(70, 36),
// ),
// onPressed: () => Navigator.pop(context),
// child: const Text("OK"),
// ),
// ],
// ),
// );
// },
),
onChanged: (List<Map<String, dynamic>> selectedVals) {
selectedHandlerIds = selectedVals
.map((v) => v['id'].toString())
.toList();
print("Selected Handler IDs: $selectedHandlerIds");
},
// popupProps: PopupProps.menu(
// fit: FlexFit.loose,
// constraints: BoxConstraints(maxHeight: 250),
// menuProps: MenuProps(
// backgroundColor:
// Colors.white, // 👈 sets dropdown background to white
// ),
// showSearchBox: true,
// searchFieldProps: TextFieldProps(
// decoration: InputDecoration(
// filled: true,
// fillColor: Colors.white,
// hintText: "Search Handler...",
// enabledBorder: OutlineInputBorder(
// borderSide: BorderSide(
// color: Colors.white,
// ), // 👈 Normal border
// ),
// focusedBorder: OutlineInputBorder(
// borderSide: BorderSide(
// color: Colors.white,
// width: 1.5,
// ), // 👈 Focused border
// ),
// ),
// ),
// // constraints: BoxConstraints(),
// ),
//
// onChanged: (val) {
// if (val != null) {
// print("Selected Handler : ${val['name']}");
// print("Id: ${val['id']}");
// selectedHandler = val['id'];
// // controllers['agentId']?.text = val['agent_code'];
// // agentId = agent['id'];
// }
// },
),
),
),
],
);
}
static final _textStyle = GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w500,
);
}

View File

@ -0,0 +1,473 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
import '../../../../core/routing/routes.dart';
import '../../../../core/services/api_service.dart';
import '../../../../data/utils/Pagination.dart';
import '../../../layouts/main_layout.dart';
import '../../../layouts/responsive_layout.dart';
import '../../../providers/manager_provider.dart';
import '../../../themes/indicators/export_btn.dart';
import '../../../themes/indicators/filter_btn.dart';
import '../../../themes/indicators/search_field_theme.dart';
import '../../../themes/indicators/text_field_theme.dart';
import '../../../widgets/custom_action_popup.dart';
class SalesExecutiveList extends ConsumerStatefulWidget {
const SalesExecutiveList({super.key});
@override
ConsumerState<SalesExecutiveList> createState() => SalesExecutiveListState();
}
class SalesExecutiveListState extends ConsumerState<SalesExecutiveList> {
int currentPage = 1;
int itemsPerPage = 10;
late ApiService apiService;
dynamic managerId;
dynamic role;
dynamic prefid;
// List<Map<String, dynamic>> dataVal = [];
List<Map<String, dynamic>> getStaffData = [];
List<Map<String, dynamic>> originalData = [];
List<Map<String, dynamic>> filteredData = [];
bool isLoading = false;
@override
void initState() {
super.initState();
apiService = ApiService();
Future.microtask(() {
final data1 = ref.read(managerIdProvider);
// final data2 = ref.read(handlerIdProvider);
print("Edata1 => mId: $data1 -2 :");
prefid = data1;
role = ref.read(userRoleProvider);
print("E43 => mId: $prefid");
if (prefid != null && role != null) {
getSalesExecutiveList(prefid, role);
}
});
// getSalesExecutiveList();
}
List<dynamic> get _paginatedData {
// Sort descending by id first
final sortedData = [...filteredData]
..sort((a, b) => int.parse(b['id']) - int.parse(a['id']));
if (sortedData.isEmpty) return [];
// Ensure currentPage is valid
final maxPage = (sortedData.length / itemsPerPage).ceil();
final safePage = currentPage.clamp(1, maxPage);
final startIndex = (safePage - 1) * itemsPerPage;
final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length);
return sortedData.sublist(startIndex, endIndex);
}
void filterData(String query) {
setState(() {
final q = query.toLowerCase();
if (q.isEmpty) {
filteredData = getStaffData;
return;
}
filteredData = getStaffData.where((item) {
final isActiveStatus = item['is_active'] == "1" ? "active" : "inactive";
return (item['name'] ?? '').toString().toLowerCase().contains(q) ||
(item['email'] ?? '').toString().toLowerCase().contains(q) ||
(item['mobile'] ?? '').toString().toLowerCase().contains(q) ||
(item['address'] ?? '').toString().toLowerCase().contains(q) ||
(item['emp_id'] ?? item['agent_code'] ?? '')
.toString()
.toLowerCase()
.contains(q) ||
isActiveStatus.contains(q);
}).toList();
});
}
final TextEditingController _searchStaffController = TextEditingController();
Future<void> getSalesExecutiveList(int id, role) async {
print('E104 => Fns called => $id');
print('E104 => Fns role => $role');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchSalesExecutiveList(id, role);
if (response['status'] == 'success') {
print('E113 => getSalesExecutiveListData => ${response['data']}');
setState(() {
getStaffData = List<Map<String, dynamic>>.from(response['data']);
originalData = getStaffData;
filteredData = List.from(originalData);
// print('originalData - $getClaimPolicies');
});
} else {
getStaffData = [];
originalData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
managerId = ref.watch(managerIdProvider);
return MainLayout(
title: "Sales Executive",
body: Container(
// padding: EdgeInsets.all(8.0),
// margin: EdgeInsets.all(10.0),
// color: Colors.yellow.shade50,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Container(
// color: Colors.green,
// color: Colors.green.shade50,
width: MediaQuery.of(context).size.width,
// margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
// color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
),
padding: EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
// height: 40,
// color: Colors.pink,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
'Sales Executive List',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Spacer(),
ThemedSearchField(
hintText: 'Search',
// backgroundColor: Color(0xFFF6F8F8),
backgroundColor: Color(0xFFFFFFFF),
txtHeight: 30,
onChanged: filterData,
controller: _searchStaffController,
txtwidth: MediaQuery.of(context).size.width * 0.15,
),
SizedBox(width: 10),
ExportBtn(
sheetName: "SalesExecutive",
fileName: "salesExecutive_list",
txt: !ResponsiveLayout.isMobile(context)
? true
: false,
data: filteredData,
displayHeaders: [
'S.No.',
'Staff Name',
'Email',
'Phone Number',
'Address',
'Status',
],
keys: [
"sno", // handled internally as i + 1
"name",
"email",
"mobile",
"address",
"is_active",
],
),
SizedBox(width: 10),
InkWell(
onTap: () {
context.go('/salesExecutive/create');
},
child: Container(
padding: EdgeInsets.all(4.8),
decoration: BoxDecoration(
color: const Color(0xFF2E7D6E),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
message: 'Create Sales Executive',
child: Icon(
Icons.add,
color: Colors.white,
size: 18,
),
),
],
),
),
),
],
),
),
SizedBox(height: 5),
Container(
decoration: BoxDecoration(
color: Color(0xFFF1F5F9),
// color: Color(0xFFEDF6F5),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
child: Row(
children: [
Expanded(
flex: 1,
child: Text('S.No.', style: _headerStyle),
),
Expanded(
flex: 2,
child: Text('Executive Name', style: _headerStyle),
),
Expanded(
flex: 3,
child: Text('Email', style: _headerStyle),
),
Expanded(
flex: 2,
child: Text('Phone Number', style: _headerStyle),
),
Expanded(
flex: 1,
child: Text('Status', style: _headerStyle),
),
Expanded(
flex: 1,
child: Text('Action', style: _headerStyle),
),
],
),
),
Expanded(child: _buildDataTable(context)),
],
),
),
),
Container(
// height: 20,
width: MediaQuery.of(context).size.width,
// color: Colors.green.shade50,
child: PaginationControls(
currentPage: currentPage,
itemsPerPage: itemsPerPage,
// totalItems: dataVal.length,
totalItems: filteredData.length,
// activeColor: layoutColor, // your theme color
onPageChanged: (page) {
setState(() {
currentPage = page;
});
},
onItemsPerPageChanged: (items) {
setState(() {
itemsPerPage = items;
currentPage = 1;
});
},
),
),
],
),
),
);
}
Widget _buildDataTable(BuildContext context) {
if (filteredData.isEmpty) {
return const SizedBox(
height: 50,
child: Center(child: Text('No available data')),
);
}
final sortedData = [..._paginatedData];
return ListView.builder(
// itemCount: filteredData.length + 1, // +1 for header, +1 for pagination
itemCount: sortedData.length + 1, // +1 for header, +1 for pagination
itemBuilder: (context, index) {
if (index == 0) return _buildHeader();
// if (index == dataVal.length + 1)
// return _buildPagination(context);
final startIndex = ((currentPage - 1) * itemsPerPage);
// final item = filteredData[index - 1];
final item = sortedData[index - 1];
final sno = startIndex + index;
return _buildDataRow(item, sno);
},
);
}
Widget _buildHeader() {
return SizedBox.shrink();
}
Widget _buildDataRow(Map<String, dynamic> item, sno) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 16),
// margin: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: Colors.white,
// color: Color(0xFFE0F7F9),
border: const Border(
bottom: BorderSide(color: Colors.blueGrey, width: 0.15),
),
// borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Expanded(flex: 1, child: Text('$sno' ?? '-', style: _dataBold)),
Expanded(flex: 2, child: Text(item['name'] ?? '-', style: _dataBold)),
Expanded(
flex: 3,
child: Text(item['email'] ?? '-', style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['mobile'] ?? '-', style: _dataBold),
),
Expanded(
flex: 1,
child: Row(
children: [
Container(
// color: Colors.yellow.shade50,
child: Transform.scale(
scale: 0.4, // reduce size (0.70.9 works well)
child: Switch(
value: item['is_active'] == "1",
onChanged: (val) {
setState(() {
item['is_active'] = val ? "1" : "0";
});
final response = apiService.updateStatus(
item['id'],
val ? "0" : "1",
'salesExecutive',
);
print("Response - $response");
},
activeColor: Color(0xFF2E7D6E), // thumb when active
activeTrackColor: Color(0xFFDCFCE7), // track when active
// activeColor: Color(0xFF425B5B), // thumb when active
// activeTrackColor: Color(0xFFB2D8D3), // track when active
inactiveThumbColor:
Colors.grey.shade400, // thumb when inactive
inactiveTrackColor:
Colors.grey.shade300, // track when inactive
),
),
),
],
),
),
Expanded(
flex: 1,
child: item['is_active'] == "1"
? Row(
children: [
Tooltip(
message: 'Edit',
child: IconButton(
icon: Image.asset(
"assets/miscellaneous/Edit.png",
height: 12,
width: 15,
),
onPressed: () {
print('EDITStaff - ${item['id']}');
dynamic id = item['id'];
context.go('/salesExecutive/$id');
},
splashRadius: 28,
hoverColor: Colors.black12,
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(),
),
),
],
)
: Row(
children: [
Image.asset(
"assets/miscellaneous/Edit_muted.png",
height: 12,
width: 15,
),
],
),
),
],
),
);
}
static final _dataBold = GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xFF000000),
);
static final _dataSub = GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w300,
color: Color(0xFF585757),
);
static final _headerStyle = GoogleFonts.poppins(
fontSize: 11.2,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
);
}

View File

@ -8,6 +8,7 @@ import 'package:nhance_partner/presentation/screens/dashboard/tabs/chart.dart';
import 'package:nhance_partner/presentation/screens/dashboard/tabs/live.dart';
import 'package:nhance_partner/presentation/screens/dashboard/tabs/partner.dart';
import 'package:nhance_partner/presentation/screens/dashboard/tabs/productivity.dart';
import 'package:nhance_partner/presentation/screens/dashboard/tabs/stacked_BAR_Exple.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/responsive/responsive_builder.dart';
import '../../../core/routing/routes.dart';
@ -135,7 +136,8 @@ class _DashboardState extends ConsumerState<Dashboard>
LiveDashboard(),
BussinessDashboard(),
PartnerDashboard(),
ProductiviyDashboard(),
ProductivityDashboard(),
// BarChartSample4(),
],
),
),

View File

@ -275,38 +275,69 @@ class _BussinessDashboardState extends ConsumerState<BussinessDashboard> {
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
children: [
Text('Hide and Show Charts : ', style: _styleSmall1),
TextButton(
onPressed: () {
setState(() => showInsurer = !showInsurer);
},
child: Text(
showInsurer ? "Insurer" : "Insurer Hidden",
style: _styleSmall2,
),
Row(
children: [
Transform.scale(
scale: 0.75,
child: Checkbox(
value: showInsurer,
onChanged: (val) {
setState(() => showInsurer = val!);
},
),
),
Text(
// showInsurer ? "Insurer" : "Insurer Hidden",
"Insurer",
style: _styleSmall2,
),
],
),
TextButton(
onPressed: () {
setState(() => showBroker = !showBroker);
},
child: Text(
showBroker ? "Broker" : "Broker Hidden",
style: _styleSmall2,
),
SizedBox(width: 10),
Row(
children: [
Transform.scale(
scale: 0.75,
child: Checkbox(
value: showBroker,
onChanged: (val) {
setState(() => showBroker = val!);
},
),
),
Text(
// showBroker ? "Broker" : "Broker Hidden",
"Broker",
style: _styleSmall2,
),
],
),
TextButton(
onPressed: () {
setState(() => showProduct = !showProduct);
},
child: Text(
showProduct ? "Product " : "Product Hidden",
style: _styleSmall2,
),
SizedBox(width: 10),
Row(
children: [
Transform.scale(
scale: 0.75,
child: Checkbox(
value: showProduct,
onChanged: (val) {
setState(() => showProduct = val!);
},
),
),
Text(
// showProduct ? "Product" : "Product Hidden",
"Product",
style: _styleSmall2,
),
],
),
],
),
@ -334,6 +365,7 @@ class _BussinessDashboardState extends ConsumerState<BussinessDashboard> {
return Container(
padding: EdgeInsets.all(10),
margin: EdgeInsets.only(right: 20),
clipBehavior: Clip.none,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
@ -364,9 +396,13 @@ class _BussinessDashboardState extends ConsumerState<BussinessDashboard> {
),
SizedBox(height: 10),
Expanded(
child: CustomBarChart(
dataList: chartInsurerData,
labels: chartInsurerLabels,
child: Container(
clipBehavior: Clip.none,
// color: Colors.red.shade100,
child: CustomBarChart(
dataList: chartInsurerData,
labels: chartInsurerLabels,
),
),
),
legendWidget(),

View File

@ -9,52 +9,51 @@ import '../../../../core/services/api_service.dart';
import '../../../../data/models/bar_data.dart';
import '../../../providers/userRoleProvider.dart';
import '../../../themes/charts/barChart.dart';
import '../../../themes/charts/stackedBarChart.dart';
class ProductiviyDashboard extends ConsumerStatefulWidget {
const ProductiviyDashboard({super.key});
class ProductivityDashboard extends ConsumerStatefulWidget {
const ProductivityDashboard({super.key});
@override
ConsumerState<ProductiviyDashboard> createState() =>
_ProductiviyDashboardState();
ConsumerState<ProductivityDashboard> createState() =>
_ProductivityDashboardState();
}
class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
class _ProductivityDashboardState extends ConsumerState<ProductivityDashboard> {
dynamic managerId;
late ApiService apiService;
// List<Map<String, dynamic>> dataVal = [];
List<Map<String, dynamic>> getBusinessDshBdData = [];
List<Map<String, dynamic>> getProductivityDshBdData = [];
List<Map<String, dynamic>> originalData = [];
List<Map<String, dynamic>> filteredData = [];
List<Map<String, dynamic>> brokerWise = [];
List<Map<String, dynamic>> productWise = [];
List<Map<String, dynamic>> insurerWise = [];
List<Map<String, dynamic>> originalBrokerWise = [];
List<Map<String, dynamic>> filteredBrokerWise = [];
List<Map<String, dynamic>> originalProductWise = [];
List<Map<String, dynamic>> filteredProductWise = [];
List<Map<String, dynamic>> originalInsurerWise = [];
List<Map<String, dynamic>> filteredInsurerWise = [];
List<Map<String, dynamic>> originalStaffWise = [];
List<Map<String, dynamic>> filteredStaffWise = [];
bool isLoading = false;
bool isInsureWisePolicy = true;
bool isBrokerWisePolicy = true;
bool isProductWisePolicy = true;
bool showInsurer = true;
bool showBroker = true;
bool showStaff = true;
bool showProduct = true;
List<String> staffLabels = [];
List<List<List<dynamic>>> staffChartData = [];
double maxPremium = 0;
int selectedView = 0;
// 0 = Insurer
// 0 = Staff
// 1 = Broker
// 2 = Product
String? SelctdBusinessbroker;
String? SelctdProductivityproduct;
@override
void initState() {
@ -70,54 +69,115 @@ class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
// print("ENQroleId - $roleId");
if (managerId != null) {
getBusinessDashBrdList(managerId);
getProductivityDashBrdList(managerId);
}
});
}
Future<void> getBusinessDashBrdList(id) async {
Future<void> getProductivityDashBrdList(id) async {
// print('getClaimList called MagId - $managerId - $role');
setState(() {
isLoading = true;
});
// dynamic id = widget.policyNumber;
print('getBusinessDshID - $id');
print('getProductivityDshID - $id');
try {
final response = await apiService.findBusinessDashboardData(
final response = await apiService.findProductivityDashboardData(
id,
SelctdBusinessbroker,
SelctdProductivityproduct,
);
if (response['status'] == 'success') {
final data = response['data'];
print('getBusinessDshBdData - ${response['data']}');
print('getProductivityDshBdData - ${response['data']}');
setState(() {
if (data is List) {
// Already a list of maps
getBusinessDshBdData = List<Map<String, dynamic>>.from(data);
} else if (data is Map) {
// Single object, wrap in a list
getBusinessDshBdData = [Map<String, dynamic>.from(data)];
} else {
getBusinessDshBdData = [];
if (data is Map<String, dynamic>) {
staffLabels.clear();
staffChartData.clear();
maxPremium = 0;
final staffWise = (data['StaffWise'] ?? []) as List<dynamic>;
staffLabels.clear();
staffChartData.clear();
maxPremium = 0.0;
double _toDouble(dynamic v) {
if (v == null) return 0.0;
if (v is double) return v;
if (v is int) return v.toDouble();
return double.tryParse(v.toString()) ?? 0.0;
}
for (var staff in staffWise) {
final name = staff['sales_executive_name']?.toString() ?? '';
staffLabels.add(name);
double totalPre = 0.0;
double totalCur = 0.0;
final products = (staff['products_list'] ?? []) as List<dynamic>;
for (var p in products) {
totalPre += _toDouble(p['total_premium_pre_month']);
totalCur += _toDouble(p['total_premium_current_month']);
}
// build 3 stacks: preMonth, currentMonth (stacked after pre), extra (difference optional)
final preStart = 0.0;
final preEnd = totalPre;
final curStart = preEnd;
final curEnd = preEnd + totalCur;
// optional extra stack (here zero, but kept for structure)
final extraStart = curEnd;
final extraEnd = curEnd; // no extra by default
// update chart max
maxPremium = [maxPremium, curEnd].reduce((a, b) => a > b ? a : b);
staffChartData.add([
[preStart, preEnd, Colors.cyan],
[curStart, curEnd, Colors.blueAccent],
// include extra only if needed >0
if ((extraEnd - extraStart) > 0)
[extraStart, extraEnd, Colors.grey],
]);
}
// use setState to save and pass to chart
setState(() {
originalStaffWise = List.from(staffWise); // if you need
staffLabels = staffLabels;
staffChartData = staffChartData;
// choose a little padding for maxY
final chartMax = (maxPremium * 1.1).ceilToDouble();
// store chartMax somewhere e.g. chartMaxY
});
setState(() {});
}
// getBusinessDshBdData = List<Map<String, dynamic>>.from(response['data']);
originalData = getBusinessDshBdData;
filteredData = List.from(originalData);
// print('originalData - $getClaimPolicies');
// if (data is List) {
// // Already a list of maps
// getProductivityDshBdData = List<Map<String, dynamic>>.from(data);
// } else if (data is Map) {
// // Single object, wrap in a list
// getProductivityDshBdData = [Map<String, dynamic>.from(data)];
// } else {
// getProductivityDshBdData = [];
// }
// // getProductivityDshBdData = List<Map<String, dynamic>>.from(response['data']);
// originalData = getProductivityDshBdData;
// filteredData = List.from(originalData);
// // print('originalData - $getClaimPolicies');
});
if (data is Map<String, dynamic>) {
setState(() {
// Store BrokerWise
brokerWise = List<Map<String, dynamic>>.from(
data['brokerWise'] ?? [],
productWise = List<Map<String, dynamic>>.from(
data['productWise'] ?? [],
);
print('brokerWise - ${brokerWise}');
originalBrokerWise = List.from(brokerWise);
filteredBrokerWise = List.from(brokerWise);
// Store ProductWise
productWise = List<Map<String, dynamic>>.from(
data['productWise'] ?? [],
@ -127,22 +187,22 @@ class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
originalProductWise = List.from(productWise);
filteredProductWise = List.from(productWise);
// Store InsurerWise
// Store StaffWise
insurerWise = List<Map<String, dynamic>>.from(
data['insurerWise'] ?? [],
);
print('insurerWise - ${insurerWise}');
originalInsurerWise = List.from(insurerWise);
filteredInsurerWise = List.from(insurerWise);
originalStaffWise = List.from(insurerWise);
filteredStaffWise = List.from(insurerWise);
});
} else {
// fallback
brokerWise = [];
productWise = [];
productWise = [];
insurerWise = [];
}
} else {
getBusinessDshBdData = [];
getProductivityDshBdData = [];
originalData = [];
}
} catch (e) {
@ -154,7 +214,7 @@ class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
}
}
List<BarData> buildInsurerBarData() {
List<BarData> buildStaffBarData() {
double parseDouble(dynamic value) {
if (value == null) return 0.0;
if (value is num) return value.toDouble();
@ -162,7 +222,7 @@ class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
return 0.0;
}
return filteredInsurerWise.map((item) {
return filteredStaffWise.map((item) {
print('policies_current - ${item['total_policies_current_month']}');
print('policies_PREV - ${item['total_policies_pre_month']}');
@ -181,8 +241,8 @@ class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
}).toList();
}
List<String> buildInsurerLabels() {
return filteredInsurerWise.map<String>((item) {
List<String> buildStaffLabels() {
return filteredStaffWise.map<String>((item) {
final name = item['short_name'];
if (name != null && name.toString().trim().isNotEmpty) {
return name.toString();
@ -192,44 +252,6 @@ class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
}).toList();
}
List<BarData> buildBrokerBarData() {
double parseDouble(dynamic value) {
if (value == null) return 0.0;
if (value is num) return value.toDouble();
if (value is String) return double.tryParse(value) ?? 0.0;
return 0.0;
}
return filteredBrokerWise.map((item) {
print('policies_current - ${item['total_policies_current_month']}');
print('policies_PREV - ${item['total_policies_pre_month']}');
print('premium_current - ${item['total_premium_current_month']}');
print('premium_PREV - ${item['total_premium_pre_month']}');
return BarData(
Colors.blue,
isBrokerWisePolicy
? parseDouble(item['total_policies_current_month'])
: parseDouble(item['total_premium_current_month']),
isBrokerWisePolicy
? parseDouble(item['total_policies_pre_month'])
: parseDouble(item['total_premium_pre_month']),
5,
);
}).toList();
}
List<String> buildBrokerLabels() {
return filteredBrokerWise.map<String>((item) {
final name = item['broker_name'];
if (name != null && name.toString().trim().isNotEmpty) {
return name.toString();
} else {
return item['broker_name']?.toString() ?? '';
}
}).toList();
}
List<BarData> buildProductBarData() {
double parseDouble(dynamic value) {
if (value == null) return 0.0;
@ -274,12 +296,196 @@ class _ProductiviyDashboardState extends ConsumerState<ProductiviyDashboard> {
margin: EdgeInsets.only(left: 10, right: 10, bottom: 10),
// color: Colors.amber.shade100,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('No Data Found ', style: _styleSmall1)],
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Hide and Show Charts : ', style: _styleSmall1),
Row(
children: [
Transform.scale(
scale: 0.75,
child: Checkbox(
value: showStaff,
onChanged: (val) {
setState(() => showStaff = val!);
},
),
),
Text(
// showStaff ? "Staff" : "Staff Hidden",
"Staff",
style: _styleSmall2,
),
],
),
// SizedBox(width: 10),
//
// Row(
// children: [
// Transform.scale(
// scale: 0.75,
// child: Checkbox(
// value: showProduct,
// onChanged: (val) {
// setState(() => showProduct = val!);
// },
// ),
// ),
// Text(
// // showBroker ? "Broker" : "Broker Hidden",
// "Product",
// style: _styleSmall2,
// ),
// ],
// ),
],
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
showStaff ? Expanded(child: insurerContainer()) : SizedBox(),
// showProduct ? Expanded(child: productContainer()) : SizedBox(),
],
),
),
],
),
);
}
Widget insurerContainer() {
final chartStaffData = buildStaffBarData();
final chartStaffLabels = buildStaffLabels();
print('chartStaffData - $chartStaffData');
print('chartStaffLabels - $chartStaffLabels');
return Container(
padding: EdgeInsets.all(10),
margin: EdgeInsets.only(right: 20),
clipBehavior: Clip.none,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Staff Wise (Previous / Current Month)',
style: _chartHeader,
),
),
Text(
isInsureWisePolicy ? "Policies" : "Premium",
style: _styleSmall1,
),
Transform.scale(
scale: 0.5,
child: Switch(
value: isInsureWisePolicy,
onChanged: (val) => setState(() => isInsureWisePolicy = val),
),
),
],
),
SizedBox(height: 10),
// Expanded(
// child: Container(
// clipBehavior: Clip.none,
// // color: Colors.red.shade100,
// child: CustomStackedBarChart(
// labels: staffLabels, // 👈 staff names
// maxY: maxPremium + 5000, // avoid touching border
// data: staffChartData,
// ),
// ),
// ),
Expanded(
child: CustomStackedBarChart(
labels: staffLabels,
data: staffChartData,
maxY: (maxPremium * 1.1).ceilToDouble(), // a bit of headroom
initialRotation: 1,
),
),
legendWidget(),
],
),
);
}
Widget productContainer() {
final chartProductData = buildProductBarData();
final chartProductLabels = buildProductLabels();
print('chartProductData - $chartProductData');
print('chartProductLabels - $chartProductLabels');
return Container(
padding: EdgeInsets.all(10),
margin: EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Staff Product Wise (Previous / Current Month)',
style: _chartHeader,
),
),
Text(
isProductWisePolicy ? "Policies" : "Premium",
style: _styleSmall1,
),
Transform.scale(
scale: 0.5,
child: Switch(
value: isProductWisePolicy,
onChanged: (val) => setState(() => isProductWisePolicy = val),
),
),
],
),
SizedBox(height: 10),
Expanded(
child: CustomBarChart(
dataList: chartProductData,
labels: chartProductLabels,
),
),
legendWidget(),
],
),
);
}
Widget legendWidget() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.circle, size: 10, color: Colors.cyan.shade300),
SizedBox(width: 2),
Text('Current Month', style: _styleSmall1),
SizedBox(width: 5),
Icon(Icons.circle, size: 10, color: Colors.orange.shade300),
SizedBox(width: 2),
Text('Previous Month', style: _styleSmall1),
],
);
}
}
final _headerStyle2 = GoogleFonts.poppins(

View File

@ -0,0 +1,177 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import '../../../themes/charts/appChartColors.dart';
class BarChartSample4 extends StatefulWidget {
BarChartSample4({super.key});
final Color dark = Colors.blueAccent;
final Color normal = AppColors.contentColorCyan;
final Color light = Colors.limeAccent;
@override
State<StatefulWidget> createState() => BarChartSample4State();
}
class BarChartSample4State extends State<BarChartSample4> {
int rotationTurns = 1;
Widget bottomTitles(double value, TitleMeta meta) {
const style = TextStyle(fontSize: 10);
String text = switch (value.toInt()) {
0 => 'Apr',
1 => 'May',
2 => 'Jun',
3 => 'Jul',
4 => 'Aug',
_ => '',
};
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
Widget leftTitles(double value, TitleMeta meta) {
if (value == meta.max) {
return Container();
}
const style = TextStyle(fontSize: 10);
return SideTitleWidget(
meta: meta,
child: Text(meta.formattedValue, style: style),
);
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.66,
child: Padding(
padding: const EdgeInsets.only(top: 16),
child: LayoutBuilder(
builder: (context, constraints) {
final barsSpace = 4.0 * constraints.maxWidth / 400;
final barsWidth = 8.0 * constraints.maxWidth / 400;
return BarChart(
BarChartData(
alignment: BarChartAlignment.center,
rotationQuarterTurns: rotationTurns,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
getTitlesWidget: bottomTitles,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: leftTitles,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
checkToShowHorizontalLine: (value) => value % 10 == 0,
getDrawingHorizontalLine: (value) => FlLine(
color: AppColors.borderColor.withValues(alpha: 0.1),
strokeWidth: 1,
),
drawVerticalLine: false,
),
borderData: FlBorderData(show: false),
groupsSpace: barsSpace,
barGroups: getData(barsWidth),
),
);
},
),
),
);
}
List<BarChartGroupData> getData(double barsWidth) {
return [
/// -------------------- 1st Group --------------------
BarChartGroupData(
x: 0,
barRods: [
/// Last Month
BarChartRodData(
toY: 20000000000,
rodStackItems: [
BarChartRodStackItem(0, 3000000000, Colors.cyan),
BarChartRodStackItem(3000000000, 11000000000, Colors.blueAccent),
BarChartRodStackItem(11000000000, 20000000000, Colors.grey),
],
width: barsWidth,
borderRadius: BorderRadius.zero,
),
/// Current Month
BarChartRodData(
toY: 24000000000,
rodStackItems: [
BarChartRodStackItem(0, 2000000000, Colors.redAccent),
BarChartRodStackItem(2000000000, 11000000000, Colors.greenAccent),
BarChartRodStackItem(
11000000000,
24000000000,
Colors.purpleAccent,
),
],
width: barsWidth,
borderRadius: BorderRadius.zero,
),
],
),
/// -------------------- 2nd Group --------------------
BarChartGroupData(
x: 1,
barRods: [
/// Last Month
BarChartRodData(
toY: 28000000000,
rodStackItems: [
BarChartRodStackItem(0, 8000000000, Colors.cyan),
BarChartRodStackItem(8000000000, 22000000000, Colors.blueAccent),
BarChartRodStackItem(22000000000, 28000000000, Colors.grey),
],
width: barsWidth,
borderRadius: BorderRadius.zero,
),
/// Current Month
BarChartRodData(
toY: 34000000000,
rodStackItems: [
BarChartRodStackItem(0, 8000000000, Colors.redAccent),
BarChartRodStackItem(8000000000, 27000000000, Colors.greenAccent),
BarChartRodStackItem(
27000000000,
34000000000,
Colors.purpleAccent,
),
],
width: barsWidth,
borderRadius: BorderRadius.zero,
),
],
),
];
}
}

View File

@ -30,24 +30,7 @@ import '../../../../themes/indicators/text_field_theme_inline_editor.dart'
hide UpperCaseTextFormatter;
class CreateProposal_Quick extends ConsumerStatefulWidget {
// final dynamic userId;
// final dynamic managerId;
// final dynamic selectedEnquiryId;
// final dynamic selectedInsurdId;
// final dynamic selectedQuotationFrmListdata;
// final dynamic selectedQuotationFrmListId;
// final void Function(String value) onSubmit;
const CreateProposal_Quick({
super.key,
// required this.userId,
// required this.managerId,
// required this.selectedEnquiryId,
// required this.selectedInsurdId,
// this.selectedQuotationFrmListdata,
// this.selectedQuotationFrmListId,
// required this.onSubmit,
});
const CreateProposal_Quick({super.key});
@override
ConsumerState<CreateProposal_Quick> createState() =>

View File

@ -3526,9 +3526,9 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
// 'Proposal Rejected': 'Awaiting Payment',
// 'Policy Created': 'Policy Created',
// 'Policy Created': 'Policy Created',
'To be assigned': 'To be assigned',
'To be assigned': 'To Be Assigned',
'Assigned': 'Assigned',
'In progress': 'In progress',
'In progress': 'In Progress',
};
List<Map<String, dynamic>> get filteredDataByTab {
@ -3654,7 +3654,7 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
SizedBox(width: 8),
Text(
// '${config['icon']} $status',
'$status',
_formatStatus(status),
style: GoogleFonts.inter(
fontSize: 13,
color: Colors.black,
@ -4000,7 +4000,9 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
SizedBox(
width: 60,
child: Text(
item['insurer_short_name'] ?? '-',
item['insurer_short_name'] != null
? item['insurer_short_name']
: '-',
style: _dataBoldthm2,
),
),
@ -4227,7 +4229,8 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
),
),
if (item['enquiry_status'] != 'Completed') ...[
if (item['enquiry_status'] != 'Completed' &&
roleId == 'manager') ...[
IconButton(
padding: EdgeInsets.zero, // remove outer padding
constraints: BoxConstraints(
@ -5405,6 +5408,17 @@ class EnquiryStaffState extends ConsumerState<EnquiryListStaffInline> {
fontSize: 12,
);
String _formatStatus(String status) {
return status
.split(' ')
.map(
(word) => word.isNotEmpty
? word[0].toUpperCase() + word.substring(1).toLowerCase()
: '',
)
.join(' ');
}
String _truncateText(String text, int limit) {
if (text.length <= limit) return text;
return '${text.substring(0, limit)}...';

View File

@ -15,7 +15,9 @@ import '../../../../data/utils/validators.dart';
import '../../../providers/manager_provider.dart';
import '../../../providers/userRoleProvider.dart';
import '../../../themes/indicators/input_field_decoration.dart';
import '../../../themes/indicators/text_field_theme_inline_editor.dart';
import '../../../themes/indicators/text_field_theme.dart';
import '../../../themes/indicators/text_field_theme_inline_editor.dart'
hide UpperCaseTextFormatter;
class RaiseEnqForm extends ConsumerStatefulWidget {
final Function(Map<String, dynamic> data) onSubmit;
@ -384,7 +386,7 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
// color: Color(0xFFFCFCFC),
// margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 25),
height: MediaQuery.of(context).size.height * 0.16,
// height: MediaQuery.of(context).size.height * 0.16,
decoration: BoxDecoration(
color: Colors.white,
// color: Colors.amber,
@ -405,27 +407,30 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
child: FocusTraversalGroup(
child: Form(
key: _formKey,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
buildAgentName(context, fromHeader: true),
if (roleId != 'staff') ...[buildSelectStaffMem(context)],
child: Container(
// color: Colors.amber.shade100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
buildAgentName(context, fromHeader: true),
if (roleId != 'staff') ...[buildSelectStaffMem(context)],
buildBroker(context),
buildInsurer(context, fromHeader: true),
buildBroker(context),
buildInsurer(context, fromHeader: true),
// buildInsuredName(context, fromHeader: true),
buildVehicleNumber(context),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
buildRefresh(context),
SizedBox(width: 5),
buildSave(context),
],
),
],
// buildInsuredName(context, fromHeader: true),
buildVehicleNumber(context),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
buildRefresh(context),
SizedBox(width: 5),
buildSave(context),
],
),
],
),
),
),
),
@ -805,22 +810,31 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
}
Widget buildVehicleNumber(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Vehicle Number *', style: _subLabelTimeStyle),
return SizedBox(
width: MediaQuery.of(context).size.width * 0.15,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Vehicle Number *', style: _subLabelTimeStyle),
SizedBox(height: 5),
SizedBox(
width: MediaQuery.of(context).size.width * 0.15,
child: ThemedFormInlineField(
SizedBox(height: 5),
ThemedFormField(
controller: controllers['regNo']!,
hintText: 'Vehicle Number',
borderColor: Color(0xFFE2E8F0),
highlightColor: Color(0xFF50A398),
padVertical: 13,
hintText: 'Vehicle Number',
txtwidth: MediaQuery.of(context).size.width * 0.26,
verticalPad: 15,
isdense: true,
validator: (value) {
// Show backend validation message
if (fieldErrors['regNo'] != null &&
fieldErrors['regNo']!.isNotEmpty) {
return fieldErrors['regNo']; // show API message
}
return Validators.requiredField(value, "regNo");
},
inputFormatters: [
UpperCaseTextFormatter(), // 👈 custom formatter for uppercase
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9- ]')),
@ -846,20 +860,54 @@ class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
});
}
},
validator: (value) {
// Show backend validation message
if (fieldErrors['regNo'] != null &&
fieldErrors['regNo']!.isNotEmpty) {
return fieldErrors['regNo']; // show API message
}
return Validators.requiredField(value, "regNo");
},
widthNone: true,
),
),
],
// ThemedFormInlineField(
// controller: controllers['regNo']!,
//
// hintText: 'Vehicle Number',
// borderColor: Color(0xFFE2E8F0),
// highlightColor: Color(0xFF50A398),
// padVertical: 13,
// isdense: true,
// inputFormatters: [
// UpperCaseTextFormatter(), // 👈 custom formatter for uppercase
// FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9- ]')),
// ],
// // validator: (value) => Validators.requiredField(value, "regNo"),
// onChanged: (val) async {
// if (_isResetting) {
// print("Skipping API because _isResetting = true");
// return;
// }
// print('Vehicle - $val');
// final res = await apiService.CheckDuplicate(context, val);
// print('Vehicle res- $res');
//
// if (res["message"] != null && res["message"] != "") {
// ToastHelper.showErrorToast(context, res['message']);
// setState(() {
// fieldErrors['regNo'] = res['message']; // store API message
// }); // 🔥 store backend message
// } else {
// setState(() {
// fieldErrors['regNo'] = res['message']; // store API message
// });
// }
// },
// validator: (value) {
// // Show backend validation message
// if (fieldErrors['regNo'] != null &&
// fieldErrors['regNo']!.isNotEmpty) {
// return fieldErrors['regNo']; // show API message
// }
//
// return Validators.requiredField(value, "regNo");
// },
// // txtheight: 30,
// // txtwidth: MediaQuery.of(context).size.width * 0.01,
// ),
],
),
);
}

View File

@ -49,12 +49,13 @@ class _CustomBarChartState extends State<CustomBarChart> {
return BarChartGroupData(
x: x,
groupVertically: false,
// barsSpace: 20,
// showingTooltipIndicators: [0],
// barsSpace: 30,
barRods: [
BarChartRodData(toY: value1, color: Colors.cyan.shade300, width: 6),
BarChartRodData(toY: value1, color: Colors.cyan.shade300, width: 4),
// BarChartRodData(toY: 2, color: Colors.orange.shade300, width: 6),
BarChartRodData(toY: value2, color: Colors.orange.shade300, width: 6),
BarChartRodData(toY: value2, color: Colors.orange.shade300, width: 4),
],
showingTooltipIndicators: touchedGroupIndex == x ? [0] : [],
);
@ -67,7 +68,72 @@ class _CustomBarChartState extends State<CustomBarChart> {
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceBetween,
// verticalAxis: Axis.horizontal,
rotationQuarterTurns: rotationTurns,
// barTouchData: BarTouchData(
// // 1. Disable built-in touch handling
// handleBuiltInTouches: false,
// touchTooltipData: BarTouchTooltipData(
// // 3. Define the tooltip content
// getTooltipItem: (group, groupIndex, rod, rodIndex) {
// // You can customize the content here
// return BarTooltipItem(
// '${rod.toY.round().toString()}', // Explicit value
// GoogleFonts.poppins(
// color: Colors.black,
// fontSize: 8,
// fontWeight: FontWeight.w500,
// ),
// );
// },
// // Customize appearance
// getTooltipColor: (BarChartGroupData group) => Colors.transparent,
// tooltipRoundedRadius: 8,
// ),
// ),
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: true,
touchTooltipData: BarTouchTooltipData(
tooltipMargin: 6,
tooltipRoundedRadius: 6,
tooltipPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
// ask fl_chart to fit tooltip inside available space
fitInsideVertically: true, // if your fl_chart version supports it
fitInsideHorizontally:
true, // if your fl_chart version supports it
getTooltipItem:
(
BarChartGroupData group,
int groupIndex,
BarChartRodData rod,
int rodIndex,
) {
final textColor = rodIndex == 0
? Colors.cyan.shade300
: Colors.orange.shade300;
final textStyle = TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w600,
);
return BarTooltipItem('${rod.toY.round()}', textStyle);
},
),
touchCallback: (FlTouchEvent e, BarTouchResponse? r) {
// optional: change state to highlight tapped group
if (r == null || r.spot == null) {
setState(() => touchedGroupIndex = -1);
return;
}
setState(() => touchedGroupIndex = r.spot!.touchedBarGroupIndex);
},
),
borderData: FlBorderData(
show: true,
border: Border(
@ -80,10 +146,14 @@ class _CustomBarChartState extends State<CustomBarChart> {
),
gridData: FlGridData(
show: true,
// drawVerticalLine: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (value) => FlLine(
color: AppColors.borderColor.withValues(alpha: 0.2),
strokeWidth: 1,
// color: AppColors.gridLinesColor.withValues(alpha: 0.2),
color: Colors.blueGrey.shade100,
// color: Colors.black,
// color: AppColors.borderColor,
strokeWidth: 0.3,
),
),
titlesData: FlTitlesData(
@ -112,8 +182,30 @@ class _CustomBarChartState extends State<CustomBarChart> {
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return Transform.rotate(
angle: -3,
child: Padding(
padding: const EdgeInsets.only(right: 3.0),
child: Text(
value.toInt().toString(),
textAlign: TextAlign.end,
style: GoogleFonts.inter(
fontSize: 9,
fontWeight: FontWeight.w400,
),
),
),
);
},
reservedSize: 45,
),
),
leftTitles: const AxisTitles(),
// rightTitles: const AxisTitles(),
topTitles: const AxisTitles(),
),
barGroups: widget.dataList.asMap().entries.map((e) {
@ -134,49 +226,8 @@ class _CustomBarChartState extends State<CustomBarChart> {
? widget.dataList!
.map((e) => e.value1 > e.value2 ? e.value1 : e.value2)
.reduce((a, b) => a > b ? a : b) +
5
15
: 20,
// barTouchData: BarTouchData(
// enabled: true,
// handleBuiltInTouches: false,
// touchTooltipData: BarTouchTooltipData(
// getTooltipColor: (group) => Colors.transparent,
// tooltipMargin: 0,
// getTooltipItem:
// (
// BarChartGroupData group,
// int groupIndex,
// BarChartRodData rod,
// int rodIndex,
// ) {
// return BarTooltipItem(
// rod.toY.toString(),
// TextStyle(
// fontWeight: FontWeight.bold,
// color: rod.color,
// fontSize: 18,
// shadows: const [
// Shadow(color: Colors.black26, blurRadius: 12),
// ],
// ),
// );
// },
// ),
// touchCallback: (event, response) {
// if (event.isInterestedForInteractions &&
// response != null &&
// response.spot != null) {
// setState(() {
// touchedGroupIndex = response.spot!.touchedBarGroupIndex;
// });
// } else {
// setState(() {
// touchedGroupIndex = -1;
// });
// }
// },
// ),
),
),
);

View File

@ -0,0 +1,165 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class CustomStackedBarChart extends StatefulWidget {
final List<String> labels;
final List<List<List<dynamic>>>
data; // data[group][stackIndex] = [start, end, color]
final double maxY;
final int initialRotation;
const CustomStackedBarChart({
super.key,
required this.labels,
required this.data,
this.maxY = 100.0,
this.initialRotation = 1,
});
@override
State<CustomStackedBarChart> createState() => _CustomStackedBarChartState();
}
class _CustomStackedBarChartState extends State<CustomStackedBarChart> {
late int rotationTurns;
@override
void initState() {
super.initState();
rotationTurns = widget.initialRotation;
}
double _toDouble(dynamic v) {
if (v == null) return 0.0;
if (v is double) return v;
if (v is int) return v.toDouble();
return double.tryParse(v.toString()) ?? 0.0;
}
BarChartGroupData _generateGroup(int xIndex) {
// Each group is a single stacked rod (one staff)
final stacks = widget.data[xIndex];
final rodStackItems = <BarChartRodStackItem>[];
for (var s in stacks) {
// s expected: [start, end, color]
final start = _toDouble(s[0]);
final end = _toDouble(s[1]);
final color = (s.length > 2 && s[2] is Color)
? s[2] as Color
: Colors.grey;
rodStackItems.add(BarChartRodStackItem(start, end, color));
}
final maxEnd = rodStackItems.isNotEmpty
? rodStackItems.map((r) => r.toY).reduce((a, b) => a > b ? a : b)
: 0.0;
return BarChartGroupData(
x: xIndex,
groupVertically: false,
barRods: [
BarChartRodData(
toY: maxEnd,
width: 12,
borderRadius: BorderRadius.zero,
rodStackItems: rodStackItems,
),
BarChartRodData(
toY: maxEnd,
width: 12,
borderRadius: BorderRadius.zero,
rodStackItems: rodStackItems,
),
],
);
}
@override
Widget build(BuildContext context) {
final maxY = widget.maxY;
return AspectRatio(
aspectRatio: 1.6,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.center,
rotationQuarterTurns: rotationTurns,
gridData: FlGridData(
show: true,
checkToShowHorizontalLine: (value) => value % (maxY / 5) == 0,
getDrawingHorizontalLine: (value) =>
FlLine(color: Colors.grey.withOpacity(0.12), strokeWidth: 0.8),
drawVerticalLine: false,
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(
enabled: true,
touchTooltipData: BarTouchTooltipData(
tooltipRoundedRadius: 6,
tooltipPadding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
fitInsideHorizontally: true,
fitInsideVertically: true,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
'${rod.toY.toStringAsFixed(2)}',
GoogleFonts.poppins(
color: Colors.black,
fontSize: 12,
fontWeight: FontWeight.w600,
),
);
},
),
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 56,
getTitlesWidget: (value, meta) {
final i = value.toInt();
if (i < 0 || i >= widget.labels.length)
return const SizedBox();
return SideTitleWidget(
meta: meta,
child: Text(
widget.labels[i],
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
);
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: GoogleFonts.inter(fontSize: 9),
);
},
),
),
leftTitles: const AxisTitles(),
topTitles: const AxisTitles(),
),
barGroups: List.generate(
widget.data.length,
(i) => _generateGroup(i),
),
maxY: maxY,
),
),
);
}
}

View File

@ -202,6 +202,7 @@ class ThemedFormField extends HookWidget {
fontWeight: FontWeight.w400, // optional
color: textColr ?? Colors.black, // optional
),
onChanged: onChanged,
),
),
);