UI_REDESIGN

This commit is contained in:
venbaittech 2025-11-19 09:43:54 +05:30
parent 7b0e14c1ba
commit eb721cd164
34 changed files with 12481 additions and 1413 deletions

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,8 @@ import 'package:nhance_partner/data/services/auth_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../../data/utils/toastNotification.dart';
import '../../presentation/screens/staff/enquiry_single_page/model/dropdown_option.dart';
import '../../presentation/screens/staff/enquiry_single_page/model/enquiry_model.dart';
import '../config/env.dart';
import 'package:universal_html/html.dart' as html;
import 'package:http/http.dart' as http;
@ -1079,4 +1081,89 @@ class ApiService {
print('fetchAGENTNameDropDown 4 - $response');
return response;
}
static late String _baseUrl;
static void initialize(String baseUrl) {
_baseUrl = Env.apiUrl;
}
// Fetch all enquiries
static Future<List<EnquiryModel>> fetchEnquiries() async {
try {
final response = await http.get(
Uri.parse('$_baseUrl/api/enquiries'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => EnquiryModel.fromJson(json)).toList();
} else {
throw Exception('Failed to load enquiries');
}
} catch (e) {
throw Exception('Error fetching enquiries: $e');
}
}
// Create new enquiry
static Future<EnquiryModel> createEnquiry(EnquiryModel enquiry) async {
try {
final response = await http.post(
Uri.parse('$_baseUrl/api/enquiries'),
headers: {'Content-Type': 'application/json'},
body: json.encode(enquiry.toJson()),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return EnquiryModel.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to create enquiry');
}
} catch (e) {
throw Exception('Error creating enquiry: $e');
}
}
// Update enquiry
static Future<EnquiryModel> updateEnquiry(
String id,
EnquiryModel enquiry,
) async {
try {
final response = await http.put(
Uri.parse('$_baseUrl/api/enquiries/$id'),
headers: {'Content-Type': 'application/json'},
body: json.encode(enquiry.toJson()),
);
if (response.statusCode == 200) {
return EnquiryModel.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to update enquiry');
}
} catch (e) {
throw Exception('Error updating enquiry: $e');
}
}
// Fetch dropdown options
static Future<List<DropdownOption>> fetchDropdownOptions(String type) async {
try {
final response = await http.get(
Uri.parse('$_baseUrl/api/options/$type'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => DropdownOption.fromJson(json)).toList();
} else {
throw Exception('Failed to load options');
}
} catch (e) {
throw Exception('Error fetching options: $e');
}
}
}

View File

@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhance_partner/presentation/providers/manager_provider.dart';
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
import 'package:nhance_partner/presentation/screens/staff/enquiry_single_page/widgets/enquiry_screen.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'core/routing/app_router.dart';
@ -110,6 +111,21 @@ class _MyAppState extends ConsumerState<MyApp> {
@override
Widget build(BuildContext context) {
// return MaterialApp(
// title: 'Nhance Partner',
// debugShowCheckedModeBanner: false,
// theme: ThemeData(
// primaryColor: const Color(0xFFb8e6e1),
// colorScheme: ColorScheme.fromSeed(
// seedColor: const Color(0xFFb8e6e1),
// primary: const Color(0xFF4a90e2),
// ),
// scaffoldBackgroundColor: const Color(0xFFf5f5f5),
// useMaterial3: true,
// ),
// home: const EnquiryScreen(),
// );
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Nhance Partner',
@ -126,6 +142,7 @@ class _MyAppState extends ConsumerState<MyApp> {
Theme.of(context).textTheme, // base on default styles
),
),
routerConfig: appRouter,
);
}

View File

@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
class AppHeader extends StatelessWidget implements PreferredSizeWidget {
const AppHeader({super.key});
@override
Size get preferredSize => const Size.fromHeight(60);
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: const Color(0xFFb8e6e1),
elevation: 2,
title: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Nhance',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF00acc1),
),
),
const Text(
'Partner',
style: TextStyle(fontSize: 10, color: Color(0xFF666666)),
),
],
),
],
),
actions: [
TextButton(
onPressed: () {},
child: const Text(
'Dashboard',
style: TextStyle(color: Colors.black87),
),
),
TextButton(
onPressed: () {},
child: const Text('Enquiry', style: TextStyle(color: Colors.black87)),
),
PopupMenuButton<String>(
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text('Users', style: TextStyle(color: Colors.black87)),
Icon(Icons.arrow_drop_down, color: Colors.black87),
],
),
),
itemBuilder: (context) => [
const PopupMenuItem(value: 'staff', child: Text('Staff')),
const PopupMenuItem(value: 'agent', child: Text('Agent')),
],
),
PopupMenuButton<String>(
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text('Reports', style: TextStyle(color: Colors.black87)),
Icon(Icons.arrow_drop_down, color: Colors.black87),
],
),
),
itemBuilder: (context) => [
const PopupMenuItem(value: 'claims', child: Text('Claims')),
const PopupMenuItem(
value: 'endorsement',
child: Text('Endorsement'),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: PopupMenuButton<String>(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
const Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'John Doe',
style: TextStyle(fontWeight: FontWeight.w600),
),
Text(
'Administrator',
style: TextStyle(fontSize: 11, color: Colors.grey),
),
],
),
const SizedBox(width: 10),
CircleAvatar(
radius: 18,
backgroundColor: const Color(0xFF4a90e2),
child: const Text(
'JD',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'profile',
child: Text('👤 Profile Details'),
),
const PopupMenuItem(value: 'logout', child: Text('🚪 Logout')),
],
),
),
],
);
}
}

View File

@ -0,0 +1,545 @@
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:jwt_decode/jwt_decode.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/routing/routes.dart';
import '../../core/services/api_service.dart';
import '../../data/services/auth_service.dart';
import '../providers/manager_provider.dart';
import '../providers/userRoleProvider.dart';
import '../screens/UserManagement/Profile/profile_web.dart';
class AppHeader extends ConsumerStatefulWidget implements PreferredSizeWidget {
const AppHeader({super.key});
@override
ConsumerState<AppHeader> createState() => _AppHeaderState();
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _AppHeaderState extends ConsumerState<AppHeader> {
String? role;
ApiService apiService = ApiService();
Map<String, dynamic>? profileData;
String? _token;
OverlayEntry? _overlayEntry;
@override
void initState() {
super.initState();
_loadUser();
_initializeToken();
}
@override
void dispose() {
_hidePopup();
super.dispose();
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
if (_token != null) {
final Map<String, dynamic> decodedToken = Jwt.parseJwt(_token!);
setState(() {
profileData = decodedToken['data'];
});
}
}
Future<void> _loadUser() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
role = prefs.getString('userRole') ?? "Guest";
});
print('AppHeader Role - $role');
}
// Clear dashboard filters
Future<void> _clearDashboardFilters() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('dashboardKeyProvider');
await prefs.remove('dashboardStatusProvider');
await prefs.remove('dashboardStaffIdProvider');
}
// Show hover popup
void _showPopup(BuildContext context, Offset offset, Size size, String key) {
_hidePopup();
_overlayEntry = OverlayEntry(
builder: (_) {
return Positioned(
left: offset.dx,
top: offset.dy + size.height + 8,
child: MouseRegion(
onExit: (_) => _hidePopup(),
child: Material(
elevation: 8,
shadowColor: Colors.black26,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (key == 'User') ...[
_buildPopupItem(
label: "Partner",
onTap: () {
_hidePopup();
context.go(AppRoutes.agentLst);
},
),
const SizedBox(height: 2),
_buildPopupItem(
label: "Staff",
onTap: () {
_hidePopup();
context.go(AppRoutes.staffLst);
},
),
],
if (key == 'Reports') ...[
if (role == 'manager') ...[
_buildPopupItem(
label: "Attendance",
onTap: () {
_hidePopup();
context.go(AppRoutes.allStaffAttendance);
},
),
],
SizedBox(height: 2),
_buildPopupItem(
label: "Claims",
onTap: () {
_hidePopup();
context.go(AppRoutes.claimlist);
},
),
const SizedBox(height: 2),
_buildPopupItem(
label: "Endorsement",
onTap: () {
_hidePopup();
context.go(AppRoutes.endosement);
},
),
const SizedBox(height: 2),
_buildPopupItem(
label: "Policy",
onTap: () {
_hidePopup();
context.go(AppRoutes.policylist);
},
),
],
],
),
),
),
),
);
},
);
Overlay.of(context, rootOverlay: true).insert(_overlayEntry!);
}
void _hidePopup() {
_overlayEntry?.remove();
_overlayEntry = null;
}
@override
Widget build(BuildContext context) {
final roleId = ref.watch(userRoleProvider);
return AppBar(
backgroundColor: const Color(0xFFD6F6F4),
elevation: 0,
automaticallyImplyLeading: false,
titleSpacing: 24,
toolbarHeight: 64,
title: Row(
children: [
// Logo
Image.asset(
"assets/login/nhance-partner-logo.png",
height: 45,
width: 95,
),
Spacer(),
// Dashboard
_buildMenuItem(
icon: Icons.dashboard,
label: "Dashboard",
onTap: () async {
_hidePopup();
await _clearDashboardFilters();
context.go(AppRoutes.dashboard);
},
),
const SizedBox(width: 15),
// Enquiry
_buildMenuItem(
icon: Icons.list_alt_rounded,
label: "Enquiry",
onTap: () async {
_hidePopup();
await _clearDashboardFilters();
if (roleId == 'agent') {
context.go(AppRoutes.enquiryLst);
} else {
context.go(AppRoutes.enquiryForStaff);
}
},
),
const SizedBox(width: 8),
// User (Manager Only) - with hover popup
if (roleId == 'manager') ...[
_buildHoverMenuItem(
icon: Icons.person_add_alt,
label: "User",
popupKey: 'User',
),
const SizedBox(width: 8),
],
// Reports (Not Staff) - with hover popup
if (roleId != 'staff') ...[
_buildHoverMenuItem(
icon: Icons.receipt_long,
label: "Reports",
popupKey: 'Reports',
),
const SizedBox(width: 8),
],
const Spacer(),
// Raise Enquiry button for agents
if (role == 'agent') ...[
Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF425B5B),
borderRadius: BorderRadius.circular(6),
),
child: InkWell(
onTap: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('enqAgentDataId');
ref.read(enquiryIdProvider.notifier).state = null;
context.go(AppRoutes.tabEnquiry);
},
child: Center(
child: Text(
'Raise Enquiry',
style: GoogleFonts.inter(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
),
),
const SizedBox(width: 16),
],
// Notifications button
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200, width: 1),
),
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.notifications_none,
color: Colors.black87,
size: 20,
),
onPressed: () {
debugPrint("Notifications tapped");
},
tooltip: 'Notifications',
),
),
const SizedBox(width: 12),
// Profile with popup
PopupMenuButton<String>(
color: Colors.white,
offset: const Offset(0, 52),
child: Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
// border: Border.all(color: Colors.grey.shade200, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 14,
// backgroundColor: const Color(0xFF4a90e2),
backgroundColor: const Color(0xFF2E7D6E),
child: Text(
profileData?['name']?.substring(0, 2).toUpperCase() ??
'P',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
profileData?['name'] ?? 'Manager',
style: GoogleFonts.inter(
fontWeight: FontWeight.w600,
fontSize: 11,
color: Colors.black87,
),
),
Text(
(role == 'agent') ? 'partner' : role ?? 'manager',
style: GoogleFonts.inter(
fontSize: 9,
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(width: 4),
Icon(
Icons.keyboard_arrow_down,
size: 16,
color: Colors.grey.shade700,
),
],
),
),
itemBuilder: (context) => [
PopupMenuItem(
enabled: false,
child: Container(
width: 480,
color: Colors.white,
child: ProfilePopUp(),
),
),
],
),
const SizedBox(width: 12),
// Logout button
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200, width: 1),
),
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.logout, color: Colors.black87, size: 18),
onPressed: () async {
debugPrint("Logout tapped");
AuthService.clearToken();
if (role != 'handler') {
await apiService.logoutUsingAPI(context);
}
context.go(AppRoutes.login);
},
tooltip: 'Logout',
),
),
],
),
);
}
// Simple menu item widget
Widget _buildMenuItem({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(6),
child: MouseRegion(
onEnter: (_) => _hidePopup(),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: Colors.black87),
const SizedBox(width: 6),
Text(
label,
style: GoogleFonts.inter(
color: Colors.black87,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
// Menu item with hover popup
Widget _buildHoverMenuItem({
required IconData icon,
required String label,
required String popupKey,
}) {
return Builder(
builder: (itemContext) {
return MouseRegion(
onEnter: (_) {
final renderBox = itemContext.findRenderObject() as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
_showPopup(context, offset, size, popupKey);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: Colors.black87),
const SizedBox(width: 6),
Text(
label,
style: GoogleFonts.inter(
color: Colors.black87,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 2),
Icon(Icons.arrow_drop_down, color: Colors.black87, size: 18),
],
),
),
);
},
);
}
// Popup item widget
Widget _buildPopupItem({required String label, required VoidCallback onTap}) {
return _HoverableContainer(
label: label,
onTap: onTap,
normalColor: Colors.white,
hoverColor: const Color(0xFFF5F5F5),
);
}
@override
Size get preferredSize => const Size.fromHeight(64);
}
// Hoverable container for popup items
class _HoverableContainer extends StatefulWidget {
final String label;
final VoidCallback onTap;
final Color normalColor;
final Color hoverColor;
const _HoverableContainer({
required this.label,
required this.onTap,
required this.normalColor,
required this.hoverColor,
});
@override
_HoverableContainerState createState() => _HoverableContainerState();
}
class _HoverableContainerState extends State<_HoverableContainer> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: InkWell(
onTap: widget.onTap,
child: Container(
width: 140,
decoration: BoxDecoration(
color: _isHovered ? widget.hoverColor : widget.normalColor,
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.label,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
Icon(
Icons.arrow_forward_ios_rounded,
size: 10,
color: Colors.grey.shade600,
),
],
),
),
),
);
}
}

View File

@ -12,6 +12,7 @@ import '../widgets/topbar.dart';
import '../widgets/footer.dart';
import '../widgets/drawer_menu.dart';
import '../widgets/mobile_tabs.dart';
import 'appheader.dart';
import 'responsive_layout.dart';
import 'package:http/http.dart' as http;
// import 'package:http/http.dart' as ref;
@ -90,70 +91,72 @@ class MainLayoutState extends ConsumerState<MainLayout> {
// Web Layout
web: Scaffold(
appBar: TopBar(
title: widget.title,
onMenuPressed: () {
// Scaffold.of(context).openDrawer();
// Scaffold.of(context).openDrawer();
setState(() {
isDrawerOpen = !isDrawerOpen;
});
},
onLogout: () async {
debugPrint("Logout tapped");
AuthService.clearToken();
if (role != 'handler') {
await apiService.logoutUsingAPI(context);
}
context.go(AppRoutes.login);
},
onProfile: (TapDownDetails details) {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
showMenu(
context: context,
color: Colors.white,
position: RelativeRect.fromRect(
details.globalPosition & const Size(40, 40),
Offset.zero & overlay.size,
),
items: [
PopupMenuItem(
enabled: false,
child: SizedBox(width: 80, child: ProfilePopUp()),
),
],
);
},
onNotifications: () {
debugPrint("Notifications tapped");
},
),
// appBar: TopBar(
// title: widget.title,
// onMenuPressed: () {
// // Scaffold.of(context).openDrawer();
// // Scaffold.of(context).openDrawer();
//
// setState(() {
// isDrawerOpen = !isDrawerOpen;
// });
// },
// onLogout: () async {
// debugPrint("Logout tapped");
// AuthService.clearToken();
// if (role != 'handler') {
// await apiService.logoutUsingAPI(context);
// }
// context.go(AppRoutes.login);
// },
//
// onProfile: (TapDownDetails details) {
// final RenderBox overlay =
// Overlay.of(context).context.findRenderObject() as RenderBox;
//
// showMenu(
// context: context,
//
// color: Colors.white,
// position: RelativeRect.fromRect(
// details.globalPosition & const Size(40, 40),
// Offset.zero & overlay.size,
// ),
// items: [
// PopupMenuItem(
// enabled: false,
// child: SizedBox(width: 480, child: ProfilePopUp()),
// ),
// ],
// );
// },
//
// onNotifications: () {
// debugPrint("Notifications tapped");
// },
// ),
appBar: AppHeader(),
body: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: isDrawerOpen ? 100 : 0, // 👈 adjust width
child: isDrawerOpen
? const DrawerMenu()
: const SizedBox.shrink(), // hides drawer
),
// AnimatedContainer(
// duration: const Duration(milliseconds: 250),
// width: isDrawerOpen ? 100 : 0, // 👈 adjust width
// child: isDrawerOpen
// ? const DrawerMenu()
// : const SizedBox.shrink(), // hides drawer
// ),
Expanded(
child: Container(
padding: const EdgeInsets.all(8.0),
color: Colors.white,
padding: const EdgeInsets.all(16.0),
color: Color(0xFFFCFDFD),
// color: Color(0xffF8FAFB),
// color: Colors.white,
child: Column(children: [Expanded(child: widget.body)]),
),
),
],
),
),
// body: Row(
// children: [
// const SizedBox(

View File

@ -164,6 +164,8 @@ class ProfilePopUpState extends ConsumerState<ProfilePopUp> {
return Container(
padding: const EdgeInsets.all(6),
width: 450,
color: Colors.white,
// color: Colors.white,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
@ -180,7 +182,8 @@ class ProfilePopUpState extends ConsumerState<ProfilePopUp> {
width: 25,
height: 25,
decoration: BoxDecoration(
color: const Color(0xFFF1F1F1),
color: Colors.white,
// color: const Color(0xFFF1F1F1),
borderRadius: BorderRadius.circular(10.0),
),
child: InkWell(
@ -212,7 +215,8 @@ class ProfilePopUpState extends ConsumerState<ProfilePopUp> {
width: 70,
height: 70,
decoration: BoxDecoration(
color: Color(0xFFD9D9D9),
// color: Color(0xFFD9D9D9),
color: Colors.white,
borderRadius: BorderRadius.circular(30.0),
),
child: Icon(
@ -429,7 +433,7 @@ class ProfilePopUpState extends ConsumerState<ProfilePopUp> {
static const _topheaderStyle = TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 19,
fontSize: 18,
);
}

View File

@ -1854,32 +1854,29 @@ class UnassignedEnq extends StatelessWidget {
focusColor: Color(0xFF3E5B56),
highlightColor: Color(0xFF3E5B56),
borderRadius: BorderRadius.circular(2), // for ripple effect
// border: Border.all(
// color: Colors.green ,
// width: 2,
// ),
onTap: (role == 'handler')
? () {
// Print the id when row is clicked
print("Clicked ID fd: ${row['id']}");
// You can also navigate or perform any action here
showDialog(
context: context,
builder: (ctx) => AssignStaffDialog(
enquiryPrimaryId: row['id'],
regNum: row['reg_no'],
userId: 1,
onSubmit: (value) {
debugPrint("New assignY: $value");
if (onRefresh != null) {
onRefresh!(); // call the parent's refresh
}
},
),
);
}
: null,
onTap: null,
// onTap: (role == 'handler')
// ? () {
// // Print the id when row is clicked
// print("Clicked ID fd: ${row['id']}");
// // You can also navigate or perform any action here
//
// showDialog(
// context: context,
// builder: (ctx) => AssignStaffDialog(
// enquiryPrimaryId: row['id'],
// regNum: row['reg_no'],
// userId: 1,
// onSubmit: (value) {
// debugPrint("New assignY: $value");
// if (onRefresh != null) {
// onRefresh!(); // call the parent's refresh
// }
// },
// ),
// );
// }
// : null,
child: Container(
// height: 5200,
margin: const EdgeInsets.symmetric(vertical: 5),

View File

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
@ -8,7 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:nhance_partner/presentation/screens/handler/quotationPopUp.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/routing/routes.dart';
@ -36,6 +38,7 @@ import '../../themes/indicators/text_field_theme_inline_editor.dart'
hide UpperCaseTextFormatter;
import '../../widgets/custom_Stdate_EnDate_Filter.dart';
import '../../widgets/custom_action_popup.dart';
import '../staff/Enquiry/quotationPopUpAccptReject.dart';
import '../staff/assignStaff.dart';
class EnquiryListHandler extends ConsumerStatefulWidget {
@ -2418,56 +2421,74 @@ class EnquiryHandlerState extends ConsumerState<EnquiryListHandler> {
// Build both-direction scrollable DataTable
return LayoutBuilder(
builder: (context, constraints) {
double minWidth = isDesktop ? constraints.maxWidth : 1300;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
double minWidth = constraints.maxWidth < 1300
? 1300
: constraints.maxWidth;
return ScrollConfiguration(
behavior: const MaterialScrollBehavior().copyWith(
dragDevices: {PointerDeviceKind.mouse, PointerDeviceKind.touch},
),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: minWidth),
child: DataTable(
headingRowColor: WidgetStatePropertyAll(Color(0xFFEDF6F5)),
dividerThickness: 0.5,
headingRowHeight: 30,
columnSpacing: isDesktop ? 20.0 : 16.0,
border: TableBorder(
horizontalInside: BorderSide(
width: 0.5,
color: Colors.grey.shade200,
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: minWidth),
child: DataTable(
headingRowColor: WidgetStatePropertyAll(Color(0xFFEDF6F5)),
dividerThickness: 0.5,
headingRowHeight: 30,
columnSpacing: isDesktop ? 20.0 : 16.0,
border: TableBorder(
horizontalInside: BorderSide(
width: 0.5,
color: Colors.grey.shade200,
),
),
columns: const [
// DataColumn(label: Text('S.No', style: _headerStyle)),
DataColumn(
label: Text('Received Date', style: _headerStyle),
),
DataColumn(label: Text('Partner *', style: _headerStyle)),
DataColumn(
label: Text('Assigned To *', style: _headerStyle),
),
DataColumn(label: Text('Broker *', style: _headerStyle)),
DataColumn(label: Text('Insurer *', style: _headerStyle)),
DataColumn(
label: Text('Insured Name *', style: _headerStyle),
),
DataColumn(
label: Text('Vehicle No *', style: _headerStyle),
),
DataColumn(
label: Text('Vehicle Type *', style: _headerStyle),
),
DataColumn(label: Text('Email', style: _headerStyle)),
DataColumn(label: Text('Mobile', style: _headerStyle)),
DataColumn(label: Text('Documents', style: _headerStyle)),
DataColumn(label: Text('Remarks', style: _headerStyle)),
DataColumn(label: Text('Action', style: _headerStyle)),
DataColumn(
label: Text('Assigned Date', style: _headerStyle),
),
DataColumn(label: Text('Premium', style: _headerStyle)),
DataColumn(
label: Text('Payment Mode', style: _headerStyle),
),
DataColumn(
label: Text('Policy Number', style: _headerStyle),
),
DataColumn(label: Text('Status', style: _headerStyle)),
],
rows: sortedData.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final sno = startIndex + index + 1;
return _buildDataRow(item, sno);
}).toList(),
),
columns: const [
// DataColumn(label: Text('S.No', style: _headerStyle)),
DataColumn(label: Text('Received Date', style: _headerStyle)),
DataColumn(label: Text('Partner *', style: _headerStyle)),
DataColumn(label: Text('Assigned To *', style: _headerStyle)),
DataColumn(label: Text('Broker *', style: _headerStyle)),
DataColumn(label: Text('Insurer *', style: _headerStyle)),
DataColumn(
label: Text('Insured Name *', style: _headerStyle),
),
DataColumn(label: Text('Vehicle No *', style: _headerStyle)),
DataColumn(
label: Text('Vehicle Type *', style: _headerStyle),
),
DataColumn(label: Text('Email', style: _headerStyle)),
DataColumn(label: Text('Mobile', style: _headerStyle)),
DataColumn(label: Text('Documents', style: _headerStyle)),
DataColumn(label: Text('Remarks', style: _headerStyle)),
DataColumn(label: Text('Action', style: _headerStyle)),
DataColumn(label: Text('Assigned Date', style: _headerStyle)),
DataColumn(label: Text('Premium', style: _headerStyle)),
DataColumn(label: Text('Payment Mode', style: _headerStyle)),
DataColumn(label: Text('Policy Number', style: _headerStyle)),
DataColumn(label: Text('Status', style: _headerStyle)),
],
rows: sortedData.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final sno = startIndex + index + 1;
return _buildDataRow(item, sno);
}).toList(),
),
),
),

View File

@ -263,9 +263,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
final message = data['data']['message'];
setState(() {
enteredEmailOrMobile = true;
});
// setState(() {
// enteredEmailOrMobile = true;
// });
if (verification == true) {
// if (switcherStatus == 1) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
import '../../../../core/config/env.dart';
import '../../../../core/services/api_service.dart';
import '../../../../data/services/auth_service.dart';
import '../../../../data/utils/toastNotification.dart';
import '../../layouts/responsive_layout.dart';
import '../../providers/manager_provider.dart';
import '../../../../../core/config/env.dart';
import '../../../../../core/services/api_service.dart';
import '../../../../../data/services/auth_service.dart';
import '../../../../../data/utils/toastNotification.dart';
import '../../../layouts/responsive_layout.dart';
import '../../../providers/manager_provider.dart';
class QuotationPopUpTab extends ConsumerStatefulWidget {
String? id;
@ -168,7 +168,7 @@ class QuotationTabState extends ConsumerState<QuotationPopUpTab> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Quotation Details',
'Proposal Details',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),

View File

@ -0,0 +1,933 @@
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/src/widgets/framework.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/services/api_service.dart';
import '../../../../data/services/auth_service.dart';
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';
class RaiseEnqForm extends ConsumerStatefulWidget {
final Function(Map<String, dynamic> data) onSubmit;
const RaiseEnqForm({super.key, required this.onSubmit});
@override
ConsumerState<RaiseEnqForm> createState() => RaiseEnqFormState();
}
class RaiseEnqFormState extends ConsumerState<RaiseEnqForm> {
late ApiService apiService;
dynamic userId;
dynamic idPrimary;
dynamic managerId;
dynamic dashboardKey;
dynamic SelectedStatus;
dynamic SelectedStaffId;
dynamic handlerId;
String? _token;
final _formKey = GlobalKey<FormState>();
Map<String, String?> fieldErrors = {};
Map<String, TextEditingController> controllers = {};
List<String> tabHeader = [
'startDate',
'endDate',
'name',
'email',
'mobile',
'code',
'address',
'regNo',
'remarks',
];
List<Map<String, dynamic>> getVehicleTypeData = [];
List<Map<String, dynamic>> filteredVechicleData = [];
List<Map<String, dynamic>> getAgentListData = [];
List<Map<String, dynamic>> filteredAgentData = [];
List<Map<String, dynamic>> getInsurersData = [];
List<Map<String, dynamic>> filteredInsurersData = [];
final GlobalKey<DropdownSearchState<Map<String, dynamic>>>
dropDownKeyInsurerEnqAsgn =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKeyBroker =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>>
dropDownSelectStaffKey =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKey =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>>
dropDownKeyInsurer = GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKeyAgent =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
List<Map<String, dynamic>> getStaffDetailsDataEnqAsgn = [];
List<Map<String, dynamic>> filteredStaffDataEnqAsgn = [];
String? selectedStaff;
String? selectedRegNum;
String? selectedStaffName;
List<Map<String, dynamic>> getBrokerData = [];
List<Map<String, dynamic>> filteredBrokerData = [];
String? selectedBroker;
String? selectedVehicleType;
String? selectedInsurer;
String? selectedAgent;
dynamic roleId;
bool isLoading = false;
Map<String, dynamic> dataDetails() {
final data = {
"agent_id": ((roleId == 'handler') || (roleId == 'manager'))
? selectedAgent
: userId,
"name": controllers["name"]?.text,
"mobile": controllers["mobile"]?.text,
"email": controllers["email"]?.text,
"reg_no": controllers["regNo"]?.text,
"vehicle_type_id": selectedVehicleType,
"is_data_created_by_handler": roleId == 'handler' ? '1' : '0',
"is_data_created_by_manager": roleId == 'manager' ? '1' : '0',
// "insurer_id": selectedInsurer,
// "rc_file_name": "rc_doc.pdf",
// "id_proof_file_name": "id_proof.pdf",
// "previous_policy_file_name": "previous_policy.pdf",
"remarks": controllers["remarks"]?.text,
"assigned_to": (roleId == 'staff') ? userId : selectedStaff,
"insurer_id": selectedInsurer,
"broker_id": selectedBroker,
"manager_id": managerId,
"created_by": userId,
};
return data;
}
@override
void initState() {
super.initState();
apiService = ApiService();
for (String field in tabHeader) {
controllers[field] = TextEditingController();
}
_initializeToken();
Future.microtask(() async {
// final id = ref.read(managerIdProvider);
roleId = ref.read(userRoleProvider);
userId = ref.read(userIdProvider);
managerId = ref.read(managerIdProvider);
final prefs = await SharedPreferences.getInstance();
dashboardKey = prefs.getString('dashboardKeyProvider');
final dashboardStatus = prefs.getString('dashboardStatusProvider');
final dashboardStaffId = prefs.getString('dashboardStaffIdProvider');
print('handlerIdENQ - $handlerId');
print("C72 => r : $roleId | uId: $userId !mID : $managerId ");
print('dashboardKey - $dashboardKey');
print('dashboardStatus - $dashboardStatus');
print('dashboardStaffId - $dashboardStaffId');
if (managerId != null) {
print('managerId - $managerId');
getAgentList(managerId);
}
final userID = ref.watch(userIdProvider);
print("managerId - $managerId");
if (userID != null) {
print('hansles');
getStaffDetailsForEnquiryAssignment(userID);
}
});
getVehicleType();
getInsurers();
}
Future<void> _initializeToken() async {
_token = await AuthService.getToken();
print("APISERTOKEN - $_token");
}
void resetFormFields() {
// 1. Reset all text controllers
controllers.forEach((key, controller) {
controller.clear();
});
// 2. Reset dropdown selections
setState(() {
selectedAgent = null;
selectedInsurer = null;
// Add any other dropdowns
});
// 3. Reset form validation
_formKey.currentState?.reset();
}
Future<void> getVehicleType() async {
print('getClaimList called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchMasterDropDown('vehicleType');
if (response['status'] == 200) {
print('getVehicleTypeData - ${response['data']}');
setState(() {
getVehicleTypeData = List<Map<String, dynamic>>.from(
response['data'],
);
print('API Data - $getVehicleTypeData');
filteredVechicleData = List.from(getVehicleTypeData);
print('originalData - $filteredVechicleData');
});
} else {
getVehicleTypeData = [];
filteredVechicleData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> getInsurers() async {
print('Insurers called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchMasterDropDown('Insurers');
if (response['status'] == 200) {
print('getInsurers - ${response['data']}');
setState(() {
getInsurersData = List<Map<String, dynamic>>.from(response['data']);
print('API Data - $getInsurersData');
filteredInsurersData = List.from(getInsurersData);
print('originalData - $filteredInsurersData');
});
} else {
getInsurersData = [];
filteredInsurersData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> getAgentList(id) async {
print('getAgentListData called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchAgentNameDropDown(id);
print('getAgentListData called response');
print('get Agent- ${response['data']}');
if (response['status'] == 'success') {
print('get Agent- ${response['data']}');
setState(() {
getAgentListData = List<Map<String, dynamic>>.from(response['data']);
print('API Data - $getAgentListData');
filteredAgentData = List.from(getAgentListData);
print('originalAgentData - $filteredAgentData');
});
} else {
getAgentListData = [];
filteredAgentData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
Future<void> getStaffDetailsForEnquiryAssignment(int id) async {
print('getStaffDetailsForEnquiryAssignment called By handler');
setState(() {
isLoading = true;
});
try {
// final response = await apiService.fetchStaffUserList(id, role);
final response = await apiService.fetchStaffListForEnquiryAssignDropDown(
id,
roleId,
);
if (response['status'] == 'success') {
print('getStaffDetailsForEnquiryAssignment - ${response['data']}');
setState(() {
getStaffDetailsDataEnqAsgn = List<Map<String, dynamic>>.from(
response['data'],
);
print('API Data - $getStaffDetailsDataEnqAsgn');
filteredStaffDataEnqAsgn = List.from(getStaffDetailsDataEnqAsgn);
print('originalData - $filteredStaffDataEnqAsgn');
});
} else {
getStaffDetailsDataEnqAsgn = [];
filteredStaffDataEnqAsgn = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
// color: Color(0xFFFCFCFC),
// margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 25),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 4,
spreadRadius: 1,
offset: const Offset(0, 2), // soft drop shadow
),
],
),
child: Form(
key: _formKey,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
buildAgentName(context, fromHeader: true),
if (roleId != 'staff') ...[buildSelectStaffMem(context)],
buildInsurer(context, fromHeader: true),
buildInsuredName(context, fromHeader: true),
buildVehicleNumber(context),
buildSave(context),
],
),
),
);
}
Widget buildAgentName(BuildContext context, {fromHeader = true}) {
Map<String, dynamic>? selectedAgntName = filteredAgentData.firstWhere(
(item) => item['id'].toString() == selectedAgent,
orElse: () => {},
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Partner', style: _subLabelTimeStyle),
SizedBox(height: 5),
SizedBox(
width: MediaQuery.of(context).size.width * 0.15,
// width: fromHeader
// ? MediaQuery.of(context).size.width * 0.11
// : MediaQuery.of(context).size.width * 0.08,
height: 30,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKeyAgent,
selectedItem: selectedAgntName.isNotEmpty ? selectedAgntName : null,
items: (filter, infiniteScrollProps) {
return filteredAgentData;
},
itemAsString: (val) => val['name'].toString(), // what to show
compareFn: (item, selectedItem) =>
item['id'] == selectedItem['id'], // compare by id
validator: (val) {
if (val == null) {
return "Required"; // error message
}
return null;
},
suffixProps: DropdownSuffixProps(
// make sure the dropdown button is visible
dropdownButtonProps: DropdownButtonProps(
isVisible: true,
padding: EdgeInsets.zero, // remove default padding
constraints: const BoxConstraints(
// shrink icon tap area
minWidth: 12,
minHeight: 12,
),
iconSize: 15, // smaller icon
// icon: const Icon(Icons.arrow_drop_down),
),
),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem != null
? selectedItem['name'].toString()
: "", // FIXED
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
),
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Partner Name",
).copyWith(
filled: true,
fillColor:
Colors.white, // 👈 makes the dropdown input white
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
),
),
popupProps: PopupProps.menu(
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
menuProps: MenuProps(
backgroundColor:
Colors.white, // 👈 sets dropdown background to white
),
showSearchBox: true,
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: "Partner Name",
hintStyle: GoogleFonts.inter(
fontSize: 10,
color: Colors.grey,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
), // 👈 Normal border
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
width: 1.5,
), // 👈 Focused border
),
),
),
// constraints: BoxConstraints(),
itemBuilder: (context, item, isDisabled, isSelected) {
return Container(
// color: isSelected ? Colors.blue.withOpacity(0.1) : null,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
item['name'].toString(),
style: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
),
Text(
item['agent_code'].toString(),
style: GoogleFonts.inter(
fontSize: 11,
color: Colors.grey,
),
),
],
),
);
},
),
onChanged: (val) {
if (val != null) {
print("Selected Partner : ${val['name']}");
print("Id: ${val['id']}");
selectedAgent = val['id'];
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
}
},
),
),
],
);
}
Widget buildInsuredName(BuildContext context, {fromHeader = true}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Insured', style: _subLabelTimeStyle),
SizedBox(height: 5),
SizedBox(
width: MediaQuery.of(context).size.width * 0.15,
child: ThemedFormInlineField(
controller: controllers['name']!,
hintText: 'Insured Name',
// errorText: fieldErrors['name'],
borderColor: Color(0xFFE2E8F0),
isdense: true,
validator: (value) => Validators.requiredField(value, "name"),
widthNone: true,
),
),
],
);
}
Widget buildInsurer(BuildContext context, {fromHeader = true}) {
Map<String, dynamic>? selectedInsurerd = filteredInsurersData.firstWhere(
(item) => item['id'].toString() == selectedInsurer,
orElse: () => {},
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Insurer', style: _subLabelTimeStyle),
SizedBox(height: 5),
Container(
height: 30,
width: MediaQuery.of(context).size.width * 0.12,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKeyInsurerEnqAsgn,
selectedItem: selectedInsurerd.isNotEmpty ? selectedInsurerd : null,
items: (filter, infiniteScrollProps) {
return filteredInsurersData;
},
itemAsString: (val) => val['name'].toString(), // what to show
compareFn: (item, selectedItem) =>
item['id'] == selectedItem['id'], // compare by id
validator: (val) {
if (val == null) {
return "Required"; // error message
}
return null;
},
suffixProps: DropdownSuffixProps(
// make sure the dropdown button is visible
dropdownButtonProps: DropdownButtonProps(
isVisible: true,
padding: EdgeInsets.zero, // remove default padding
constraints: const BoxConstraints(
// shrink icon tap area
minWidth: 12,
minHeight: 12,
),
iconSize: 15, // smaller icon
// icon: const Icon(Icons.arrow_drop_down),
),
),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem != null ? selectedItem['name'].toString() : "",
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
),
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Insurer",
).copyWith(
filled: true,
fillColor:
Colors.white, // 👈 makes the dropdown input white
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
),
),
popupProps: PopupProps.menu(
fit: FlexFit.loose,
constraints: BoxConstraints(maxHeight: 250),
menuProps: MenuProps(
backgroundColor:
Colors.white, // 👈 sets dropdown background to white
),
showSearchBox: true,
searchFieldProps: TextFieldProps(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: "Search Insurer...",
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
), // 👈 Normal border
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
width: 1.5,
), // 👈 Focused border
),
),
),
itemBuilder: (context, item, isDisabled, isSelected) {
return Container(
// color: isSelected ? Colors.blue.withOpacity(0.1) : null,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
child: Text(
item['name'].toString(),
style: GoogleFonts.inter(fontSize: 12, color: Colors.black),
),
);
},
// constraints: BoxConstraints(),
),
onChanged: (val) {
if (val != null) {
print("Selected Insurer : ${val['name']}");
print("Id: ${val['id']}");
selectedInsurer = val['id'];
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
}
},
),
),
],
);
}
Widget buildVehicleNumber(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Vehicle Number', style: _subLabelTimeStyle),
SizedBox(height: 5),
SizedBox(
width: MediaQuery.of(context).size.width * 0.15,
child: ThemedFormInlineField(
controller: controllers['regNo']!,
hintText: 'Vehicle Number',
borderColor: Color(0xFFE2E8F0),
isdense: true,
inputFormatters: [
UpperCaseTextFormatter(), // 👈 custom formatter for uppercase
FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9- ]')),
],
validator: (value) => Validators.requiredVechileNum(value, "regNo"),
widthNone: true,
),
),
],
);
}
Widget buildSelectStaffMem(ctx) {
Map<String, dynamic>? selectedVehicle;
try {
selectedVehicle = filteredStaffDataEnqAsgn.firstWhere(
(item) => item['id'].toString() == selectedStaff,
);
} catch (e) {
selectedVehicle = null; // fallback
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Assigned To', style: _subLabelTimeStyle),
SizedBox(height: 5),
SizedBox(
height: 30,
width: MediaQuery.of(context).size.width * 0.15,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownSelectStaffKey,
// selectedItem: selectedVehicle.isNotEmpty ? selectedVehicle : null,
selectedItem: selectedVehicle,
items: (filter, infiniteScrollProps) {
return filteredStaffDataEnqAsgn;
},
itemAsString: (val) => val['name'].toString(),
compareFn: (item, selectedItem) =>
item['id'] == selectedItem['id'], // compare by id
validator: (val) {
if (val == null) {
return "Required"; // error message
}
return null;
},
suffixProps: DropdownSuffixProps(
// make sure the dropdown button is visible
dropdownButtonProps: DropdownButtonProps(
isVisible: true,
padding: EdgeInsets.zero, // remove default padding
constraints: const BoxConstraints(
// shrink icon tap area
minWidth: 12,
minHeight: 12,
),
iconSize: 15, // smaller icon
// icon: const Icon(Icons.arrow_drop_down),
),
),
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Select Staff Member",
).copyWith(
filled: true,
fillColor:
Colors.white, // 👈 makes the dropdown input white
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Color(0xFFE2E8F0),
width: 0.5,
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
),
),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem != null ? selectedItem['name'].toString() : "",
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
),
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 Staff ...",
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
), // 👈 Normal border
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
width: 1.5,
), // 👈 Focused border
),
),
),
// constraints: BoxConstraints(),
itemBuilder: (context, item, isDisabled, isSelected) {
return Container(
// color: isSelected ? Colors.blue.withOpacity(0.1) : null,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
child: Text(
item['name'].toString(),
style: GoogleFonts.inter(fontSize: 12, color: Colors.black),
),
);
},
),
onChanged: (val) {
if (val != null) {
print("Selected Staff : ${val['name']}");
print("Id: ${val['id']}");
selectedStaffName = val['name'];
selectedStaff = val['id'];
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
}
},
),
),
],
);
}
Widget buildSave(context) {
return InkWell(
onTap: () {
if (!_formKey.currentState!.validate()) return;
final dataSet = dataDetails();
widget.onSubmit(dataSet);
resetFields();
},
child: Container(
padding: EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: const Color(0xFF2E7D6E),
borderRadius: BorderRadius.circular(5.0),
),
child: Text('Create', style: GoogleFonts.inter(color: Colors.white)),
),
);
}
void resetFields() {
print('Field ReSET');
// 1. Reset all text controllers
controllers.forEach((key, controller) {
controller.clear();
});
// 2. Reset dropdown selections
setState(() {
selectedAgent = null;
selectedInsurer = null;
selectedStaff = null;
selectedStaffName = null;
selectedVehicleType = null;
selectedRegNum = null;
selectedBroker = null;
});
// 3. Reset dropdown UI (important for DropdownSearch)
dropDownKeyAgent.currentState?.clear();
dropDownKeyInsurerEnqAsgn.currentState?.clear();
dropDownSelectStaffKey.currentState?.clear();
dropDownKeyInsurer.currentState?.clear();
dropDownKey.currentState?.clear();
dropDownKeyBroker.currentState?.clear();
// 4. Reset validators
_formKey.currentState?.reset();
}
final _subLabelTimeStyle = GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
);
}

View File

@ -31,6 +31,7 @@ import '../../../../providers/quotation_staff_proivder.dart';
import '../../../../themes/indicators/custom_loader.dart';
import '../../../../themes/indicators/customizd_file_upload.dart';
import '../../../../themes/indicators/date_field_theme.dart';
import '../../../../themes/indicators/input_field_decoration.dart';
import '../../../../themes/indicators/text_field_theme.dart';
class PolicyStaffTab extends ConsumerStatefulWidget {
@ -52,6 +53,14 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKey =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
final GlobalKey<DropdownSearchState<Map<String, dynamic>>> dropDownKeyBroker =
GlobalKey<DropdownSearchState<Map<String, dynamic>>>();
List<Map<String, dynamic>> getBrokerData = [];
List<Map<String, dynamic>> filteredBrokerData = [];
String? selectedBroker;
String? selectedBrokerName;
List<String> tabHeader = [
'name',
'email',
@ -143,7 +152,7 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
// "premium_amount": controllers["policyPremiumAmount"]?.text,
"policy_number": controllers["policyNumber"]?.text,
"payment_mode": controllers["policyPaymentMode"]?.text,
"broker_id": selectedBroker,
"tp": controllers["tp"]?.text,
"od": controllers["od"]?.text,
"pa": controllers["pa"]?.text,
@ -214,7 +223,7 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
controllers["cgst"]?.text = '0';
controllers["sgst"]?.text = '0';
controllers["igst"]?.text = '0';
getBroker();
if (enqQuotation != null) {
_loadData(enqQuotation);
}
@ -321,6 +330,37 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
}
}
Future<void> getBroker() async {
print('getBroker called');
setState(() {
isLoading = true;
});
try {
final response = await apiService.fetchMasterDropDown('Broker');
if (response['status'] == 200) {
print('getBroker - ${response['data']}');
setState(() {
getBrokerData = List<Map<String, dynamic>>.from(response['data']);
print('API Data - $getBrokerData');
filteredBrokerData = List.from(getBrokerData);
print('originalData - $filteredBrokerData');
});
} else {
getBrokerData = [];
filteredBrokerData = [];
}
} catch (e) {
print('Exception occurred: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
void updateEnquiryData(enquiryData) {
setState(() {
selectedEnquiryId = enquiryData['id'];
@ -394,6 +434,7 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
controllers["premiumTOT"]?.text =
policyData['premium_amount']?.toString() ?? '';
selectedBroker = policyData['broker_id'] ?? '';
// RC FILE
String? rcPath = policyData["policy_pdf_file_name"];
if (rcPath != null && rcPath.isNotEmpty) {
@ -490,13 +531,14 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
final bool isUpdating = choosedPolcyId != null;
if (!isUpdating && enqBrokerName == 'Nhance') {
if (!isUpdating && selectedBrokerName == 'Nhance') {
// if (!isUpdating && enqBrokerName == 'Nhance') {
ToastHelper.showErrorToast(context, "Policy PDF is required");
return;
}
// (enqBrokerName != 'Nhance')
// File validations
if ((enqBrokerName != 'Nhance') &&
if ((selectedBrokerName != 'Nhance') &&
(docPDFUploadedFile == null || docPDFUploadedFile?.bytes == null) &&
(docUploadedPDFFileUrlFromApi == null ||
docUploadedPDFFileUrlFromApi!.isEmpty)) {
@ -1022,54 +1064,62 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildUploadPolicyPdf(context),
SizedBox(width: 20),
Paymentmode(context),
],
),
SizedBox(height: 10),
Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
policynumber(context),
SizedBox(width: 20),
policyissuedate(context),
SizedBox(width: 20),
policystartdate(context),
SizedBox(width: 20),
policyenddate(context),
buildBroker(context),
if (selectedBrokerName != null) ...[
SizedBox(width: 20),
buildUploadPolicyPdf(context),
SizedBox(width: 20),
Paymentmode(context),
],
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
if (selectedBrokerName != null) ...[
SizedBox(height: 10),
Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
policynumber(context),
SizedBox(width: 20),
policyissuedate(context),
SizedBox(width: 20),
policystartdate(context),
SizedBox(width: 20),
policyenddate(context),
],
),
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildThirdParty(context),
buildOwnDamage(context),
buildPersonalAccident(context),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(width: 20),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildCGST(context),
buildSGST(context),
buildIGST(context),
buildPremiumTotal(context),
],
),
],
),
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildThirdParty(context),
buildOwnDamage(context),
buildPersonalAccident(context),
],
),
SizedBox(width: 20),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildCGST(context),
buildSGST(context),
buildIGST(context),
buildPremiumTotal(context),
],
),
],
),
],
],
),
);
@ -1348,6 +1398,154 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
);
}
Widget buildBroker(BuildContext context) {
Map<String, dynamic>? selectedBrokers = filteredBrokerData.firstWhere(
(item) => item['id'].toString() == selectedBroker,
orElse: () => {},
);
return Row(
children: [
Text('Broker *', style: _textStyle),
SizedBox(width: 15),
SizedBox(
height: 30,
width: MediaQuery.of(context).size.width * 0.1,
child: DropdownSearch<Map<String, dynamic>>(
key: dropDownKeyBroker,
selectedItem: selectedBrokers.isNotEmpty ? selectedBrokers : null,
items: (filter, infiniteScrollProps) {
return filteredBrokerData;
},
itemAsString: (val) => val['name'].toString(), // what to show
compareFn: (item, selectedItem) =>
item['id'] == selectedItem['id'], // compare by id
validator: (val) {
if (val == null) {
return "Required"; // error message
}
return null;
},
suffixProps: DropdownSuffixProps(
// make sure the dropdown button is visible
dropdownButtonProps: DropdownButtonProps(
isVisible: true,
padding: EdgeInsets.zero, // remove default padding
constraints: const BoxConstraints(
// shrink icon tap area
minWidth: 12,
minHeight: 12,
),
iconSize: 15, // smaller icon
// icon: const Icon(Icons.arrow_drop_down),
),
),
dropdownBuilder: (context, selectedItem) => Align(
alignment: Alignment.centerLeft,
child: Text(
selectedItem != null ? selectedItem['name'].toString() : "",
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
),
decoratorProps: DropDownDecoratorProps(
decoration:
AppInputDecorations.dropdownDecoration(
label: "Select Broker",
).copyWith(
filled: true,
fillColor:
Colors.white, // 👈 makes the dropdown input white
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.black,
width: 0.1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.black,
width: 0.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(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: "Search Broker...",
hintStyle: GoogleFonts.inter(
fontSize: 12,
color: Colors.black,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
), // 👈 Normal border
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
width: 1.5,
), // 👈 Focused border
),
),
),
itemBuilder: (context, item, isDisabled, isSelected) {
return Container(
// color: isSelected ? Colors.blue.withOpacity(0.1) : null,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
child: Text(
item['name'].toString(),
style: GoogleFonts.inter(fontSize: 12, color: Colors.black),
),
);
},
// constraints: BoxConstraints(),
),
onChanged: (val) {
if (val != null) {
print("Selected Broker : ${val['name']}");
print("Id: ${val['id']}");
selectedBroker = val['id'];
setState(() {
selectedBrokerName = val['name'];
print('selectedBrokerName - $selectedBrokerName');
});
// controllers['agentId']?.text = val['agent_code'];
// agentId = agent['id'];
}
},
),
),
],
);
}
Widget Paymentmode(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
@ -1365,7 +1563,7 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
Validators.requiredField(value, "policyPaymentMode"),
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.15,
: MediaQuery.of(context).size.width * 0.07,
),
],
);
@ -1535,7 +1733,7 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.15,
width: MediaQuery.of(context).size.width * 0.12,
child: Text('Upload Policy PDF * ', style: _textStyle),
),
// const SizedBox(height: 10),
@ -1577,7 +1775,7 @@ class PolicyStaffTabState extends ConsumerState<PolicyStaffTab> {
docPDFUploadedFile = file;
});
if (enqBrokerName == 'Nhance') {
if (selectedBrokerName == 'Nhance') {
handleUploadPolicy();
}
},

View File

@ -229,8 +229,8 @@ class TabEnquiryStaffListState extends ConsumerState<TabEnquiryStaffList> {
children: [
insured_name(context),
vehcile_num(context),
vehcile_Type(context),
broker(context),
// vehcile_Type(context),
// broker(context),
SizedBox.shrink(),
],
),

View File

@ -0,0 +1,13 @@
class DropdownOption {
final String value;
final String label;
DropdownOption({required this.value, required this.label});
factory DropdownOption.fromJson(Map<String, dynamic> json) {
return DropdownOption(
value: json['value'] ?? '',
label: json['label'] ?? json['value'] ?? '',
);
}
}

View File

@ -0,0 +1,99 @@
class EnquiryModel {
final String? id;
final DateTime receivedDate;
final String partner;
final String assignedTo;
final String broker;
final String insurer;
final String insuredName;
final String vehicleNo;
final String vehicleType;
final String email;
final String mobile;
final String remarks;
final String status;
EnquiryModel({
this.id,
required this.receivedDate,
required this.partner,
required this.assignedTo,
required this.broker,
required this.insurer,
required this.insuredName,
required this.vehicleNo,
required this.vehicleType,
required this.email,
required this.mobile,
required this.remarks,
required this.status,
});
factory EnquiryModel.fromJson(Map<String, dynamic> json) {
return EnquiryModel(
id: json['id']?.toString(),
receivedDate: DateTime.parse(json['received_date']),
partner: json['partner'] ?? '',
assignedTo: json['assigned_to'] ?? '',
broker: json['broker'] ?? '',
insurer: json['insurer'] ?? '',
insuredName: json['insured_name'] ?? '',
vehicleNo: json['vehicle_no'] ?? '',
vehicleType: json['vehicle_type'] ?? '',
email: json['email'] ?? '',
mobile: json['mobile'] ?? '',
remarks: json['remarks'] ?? '',
status: json['status'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'received_date': receivedDate.toIso8601String(),
'partner': partner,
'assigned_to': assignedTo,
'broker': broker,
'insurer': insurer,
'insured_name': insuredName,
'vehicle_no': vehicleNo,
'vehicle_type': vehicleType,
'email': email,
'mobile': mobile,
'remarks': remarks,
'status': status,
};
}
EnquiryModel copyWith({
String? id,
DateTime? receivedDate,
String? partner,
String? assignedTo,
String? broker,
String? insurer,
String? insuredName,
String? vehicleNo,
String? vehicleType,
String? email,
String? mobile,
String? remarks,
String? status,
}) {
return EnquiryModel(
id: id ?? this.id,
receivedDate: receivedDate ?? this.receivedDate,
partner: partner ?? this.partner,
assignedTo: assignedTo ?? this.assignedTo,
broker: broker ?? this.broker,
insurer: insurer ?? this.insurer,
insuredName: insuredName ?? this.insuredName,
vehicleNo: vehicleNo ?? this.vehicleNo,
vehicleType: vehicleType ?? this.vehicleType,
email: email ?? this.email,
mobile: mobile ?? this.mobile,
remarks: remarks ?? this.remarks,
status: status ?? this.status,
);
}
}

View File

@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import '../model/dropdown_option.dart';
import '../model/enquiry_model.dart';
import '../widgets/searchable_dropdown.dart';
class EnquiryForm extends StatefulWidget {
final Function(EnquiryModel) onSubmit;
final List<DropdownOption> partners;
final List<DropdownOption> insurers;
final List<DropdownOption> assignees;
const EnquiryForm({
super.key,
required this.onSubmit,
required this.partners,
required this.insurers,
required this.assignees,
});
@override
State<EnquiryForm> createState() => _EnquiryFormState();
}
class _EnquiryFormState extends State<EnquiryForm> {
final _formKey = GlobalKey<FormState>();
String? _selectedPartner;
String? _selectedInsurer;
String? _selectedAssignee;
final _insuredNameController = TextEditingController();
final _regNoController = TextEditingController();
@override
void dispose() {
_insuredNameController.dispose();
_regNoController.dispose();
super.dispose();
}
void _submit() {
if (_formKey.currentState!.validate()) {
final enquiry = EnquiryModel(
receivedDate: DateTime.now(),
partner: _selectedPartner!,
assignedTo: _selectedAssignee!,
broker: 'Nhance',
insurer: _selectedInsurer!,
insuredName: _insuredNameController.text,
vehicleNo: _regNoController.text,
vehicleType: '',
email: '',
mobile: '',
remarks: '',
status: 'awaiting',
);
widget.onSubmit(enquiry);
// Reset form
_formKey.currentState!.reset();
_insuredNameController.clear();
_regNoController.clear();
setState(() {
_selectedPartner = null;
_selectedInsurer = null;
_selectedAssignee = null;
});
}
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Form(
key: _formKey,
child: Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.start,
children: [
SizedBox(
width: 180,
child: SearchableDropdown(
label: 'Partner',
value: _selectedPartner,
items: widget.partners,
onChanged: (value) => setState(() => _selectedPartner = value),
validator: (value) => value == null ? 'Required' : null,
),
),
SizedBox(
width: 180,
child: SearchableDropdown(
label: 'Insurer',
value: _selectedInsurer,
items: widget.insurers,
onChanged: (value) => setState(() => _selectedInsurer = value),
validator: (value) => value == null ? 'Required' : null,
),
),
SizedBox(
width: 180,
child: TextFormField(
controller: _insuredNameController,
decoration: const InputDecoration(
labelText: 'Insured Name',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
),
style: const TextStyle(fontSize: 13),
validator: (value) =>
value?.isEmpty ?? true ? 'Required' : null,
),
),
SizedBox(
width: 180,
child: TextFormField(
controller: _regNoController,
decoration: const InputDecoration(
labelText: 'Reg No',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
),
style: const TextStyle(fontSize: 13),
validator: (value) =>
value?.isEmpty ?? true ? 'Required' : null,
),
),
SizedBox(
width: 180,
child: SearchableDropdown(
label: 'Assigned To',
value: _selectedAssignee,
items: widget.assignees,
onChanged: (value) => setState(() => _selectedAssignee = value),
validator: (value) => value == null ? 'Required' : null,
),
),
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4a90e2),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
),
child: const Text('Create', style: TextStyle(fontSize: 13)),
),
],
),
),
);
}
}

View File

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import '../model/dropdown_option.dart';
import '../model/enquiry_model.dart';
import 'enquiry_row.dart';
class EnquiryGroup extends StatefulWidget {
final String title;
final String icon;
final String groupKey;
final List<EnquiryModel> enquiries;
final Function(EnquiryModel) onUpdate;
final List<DropdownOption> partners;
final List<DropdownOption> insurers;
final List<DropdownOption> assignees;
const EnquiryGroup({
super.key,
required this.title,
required this.icon,
required this.groupKey,
required this.enquiries,
required this.onUpdate,
required this.partners,
required this.insurers,
required this.assignees,
});
@override
State<EnquiryGroup> createState() => _EnquiryGroupState();
}
class _EnquiryGroupState extends State<EnquiryGroup> {
bool _isExpanded = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
InkWell(
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: Container(
color: const Color(0xFFf8f9fa),
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(
_isExpanded ? Icons.arrow_drop_down : Icons.arrow_right,
size: 24,
),
const SizedBox(width: 8),
Text(
'${widget.icon} ${widget.title}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFe3f2fd),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${widget.enquiries.length} Records',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF1976d2),
),
),
),
],
),
),
),
if (_isExpanded)
...widget.enquiries.isEmpty
? [
const Padding(
padding: EdgeInsets.all(30),
child: Text(
'No records found',
style: TextStyle(
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
),
]
: widget.enquiries.map((enquiry) {
return EnquiryRow(
enquiry: enquiry,
onUpdate: widget.onUpdate,
partners: widget.partners,
insurers: widget.insurers,
assignees: widget.assignees,
);
}).toList(),
],
);
}
}

View File

@ -0,0 +1,255 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../model/dropdown_option.dart';
import '../model/enquiry_model.dart';
import '../widgets/searchable_dropdown.dart';
class EnquiryRow extends StatefulWidget {
final EnquiryModel enquiry;
final Function(EnquiryModel) onUpdate;
final List<DropdownOption> partners;
final List<DropdownOption> insurers;
final List<DropdownOption> assignees;
const EnquiryRow({
super.key,
required this.enquiry,
required this.onUpdate,
required this.partners,
required this.insurers,
required this.assignees,
});
@override
State<EnquiryRow> createState() => _EnquiryRowState();
}
class _EnquiryRowState extends State<EnquiryRow> {
bool _isEditMode = false;
late EnquiryModel _editedEnquiry;
@override
void initState() {
super.initState();
_editedEnquiry = widget.enquiry;
}
void _toggleEditMode() {
setState(() {
if (_isEditMode) {
widget.onUpdate(_editedEnquiry);
}
_isEditMode = !_isEditMode;
});
}
Widget _buildCell(String value, {Function(String)? onChanged}) {
if (_isEditMode && onChanged != null) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFe3f2fd),
border: Border.all(color: const Color(0xFF4a90e2), width: 1),
borderRadius: BorderRadius.circular(3),
),
child: TextFormField(
initialValue: value,
style: const TextStyle(fontSize: 12),
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.all(4),
),
onChanged: onChanged,
),
);
}
return Text(value, style: const TextStyle(fontSize: 12));
}
Widget _buildDropdownCell(
String value,
List<DropdownOption> options,
Function(String?) onChanged,
) {
if (_isEditMode) {
return Container(
constraints: const BoxConstraints(minHeight: 36),
child: SearchableDropdown(
label: '',
value: value,
items: options,
onChanged: onChanged,
),
);
}
return Text(value, style: const TextStyle(fontSize: 12));
}
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('dd-MM-yyyy\nhh:mm a');
return Container(
decoration: BoxDecoration(
color: _isEditMode ? const Color(0xFFf8f9fa) : Colors.white,
border: const Border(bottom: BorderSide(color: Color(0xFFf0f0f0))),
),
child: IntrinsicHeight(
child: Row(
children: [
SizedBox(
width: 120,
child: Padding(
padding: const EdgeInsets.all(10),
child: Text(
dateFormat.format(widget.enquiry.receivedDate),
style: const TextStyle(fontSize: 12),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildDropdownCell(
_editedEnquiry.partner,
widget.partners,
(v) => setState(
() => _editedEnquiry = _editedEnquiry.copyWith(partner: v),
),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildDropdownCell(
_editedEnquiry.assignedTo,
widget.assignees,
(v) => setState(
() =>
_editedEnquiry = _editedEnquiry.copyWith(assignedTo: v),
),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCell(_editedEnquiry.broker),
),
),
SizedBox(
width: 100,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildDropdownCell(
_editedEnquiry.insurer,
widget.insurers,
(v) => setState(
() => _editedEnquiry = _editedEnquiry.copyWith(insurer: v),
),
),
),
),
SizedBox(
width: 150,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCell(
_editedEnquiry.insuredName,
onChanged: (v) =>
_editedEnquiry = _editedEnquiry.copyWith(insuredName: v),
),
),
),
SizedBox(
width: 120,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCell(
_editedEnquiry.vehicleNo,
onChanged: (v) =>
_editedEnquiry = _editedEnquiry.copyWith(vehicleNo: v),
),
),
),
SizedBox(
width: 120,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCell(
_editedEnquiry.vehicleType,
onChanged: (v) =>
_editedEnquiry = _editedEnquiry.copyWith(vehicleType: v),
),
),
),
SizedBox(
width: 150,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCell(
_editedEnquiry.email,
onChanged: (v) =>
_editedEnquiry = _editedEnquiry.copyWith(email: v),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCell(
_editedEnquiry.mobile,
onChanged: (v) =>
_editedEnquiry = _editedEnquiry.copyWith(mobile: v),
),
),
),
SizedBox(
width: 150,
child: Padding(
padding: const EdgeInsets.all(10),
child: _buildCell(
_editedEnquiry.remarks,
onChanged: (v) =>
_editedEnquiry = _editedEnquiry.copyWith(remarks: v),
),
),
),
SizedBox(
width: 80,
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
IconButton(
icon: Text(_isEditMode ? '💾' : '✏️'),
onPressed: _toggleEditMode,
tooltip: _isEditMode ? 'Save' : 'Edit',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(width: 8),
IconButton(
icon: const Text('📄'),
onPressed: () {},
tooltip: 'Documents',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,413 @@
import 'package:flutter/material.dart';
import '../../../../../core/services/api_service.dart';
import '../../../../layouts/appheader.dart';
import '../model/dropdown_option.dart';
import '../model/enquiry_model.dart';
import '../screens/enquiry_form.dart';
import '../screens/enquiry_group.dart';
class EnquiryScreen extends StatefulWidget {
const EnquiryScreen({super.key});
@override
State<EnquiryScreen> createState() => _EnquiryScreenState();
}
class _EnquiryScreenState extends State<EnquiryScreen> {
List<EnquiryModel> _enquiries = [];
List<DropdownOption> _partners = [];
List<DropdownOption> _insurers = [];
List<DropdownOption> _assignees = [];
bool _isLoading = true;
String _searchQuery = '';
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
try {
final results = await Future.wait([
ApiService.fetchEnquiries(),
ApiService.fetchDropdownOptions('partners'),
ApiService.fetchDropdownOptions('insurers'),
ApiService.fetchDropdownOptions('assignees'),
]);
setState(() {
_enquiries = results[0] as List<EnquiryModel>;
_partners = results[1] as List<DropdownOption>;
_insurers = results[2] as List<DropdownOption>;
_assignees = results[3] as List<DropdownOption>;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error loading data: $e')));
}
}
Future<void> _createEnquiry(EnquiryModel enquiry) async {
try {
final created = await ApiService.createEnquiry(enquiry);
setState(() {
_enquiries.insert(0, created);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enquiry created successfully')),
);
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error creating enquiry: $e')));
}
}
Future<void> _updateEnquiry(EnquiryModel enquiry) async {
try {
final updated = await ApiService.updateEnquiry(enquiry.id!, enquiry);
setState(() {
final index = _enquiries.indexWhere((e) => e.id == enquiry.id);
if (index != -1) {
_enquiries[index] = updated;
}
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enquiry updated successfully')),
);
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error updating enquiry: $e')));
}
}
Map<String, List<EnquiryModel>> _groupEnquiries() {
final filtered = _enquiries.where((e) {
if (_searchQuery.isEmpty) return true;
final query = _searchQuery.toLowerCase();
return e.partner.toLowerCase().contains(query) ||
e.insuredName.toLowerCase().contains(query) ||
e.vehicleNo.toLowerCase().contains(query) ||
e.email.toLowerCase().contains(query) ||
e.mobile.contains(query);
}).toList();
return {
'awaiting': filtered.where((e) => e.status == 'awaiting').toList(),
'created': filtered.where((e) => e.status == 'created').toList(),
'accepted': filtered.where((e) => e.status == 'accepted').toList(),
'rejected': filtered.where((e) => e.status == 'rejected').toList(),
'policy': filtered.where((e) => e.status == 'policy').toList(),
};
}
@override
Widget build(BuildContext context) {
final groupedEnquiries = _groupEnquiries();
return Scaffold(
appBar: const AppHeader(),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
EnquiryForm(
onSubmit: _createEnquiry,
partners: _partners,
insurers: _insurers,
assignees: _assignees,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'📋 Enquiries',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
),
),
SizedBox(
width: 250,
child: TextField(
decoration: const InputDecoration(
hintText: 'Search...',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
onChanged: (value) {
setState(() => _searchQuery = value);
},
),
),
],
),
),
const SizedBox(height: 15),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: SingleChildScrollView(
child: Column(
children: [
// Table Header
Container(
color: const Color(0xFFf8f9fa),
child: const Row(
children: [
SizedBox(
width: 120,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Received Date',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Partner',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Assigned To',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Broker',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Insurer',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 150,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Insured Name',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 120,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Vehicle No',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 120,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Vehicle Type',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 150,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Email',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 100,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Mobile',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 150,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Remarks',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
SizedBox(
width: 80,
child: Padding(
padding: EdgeInsets.all(10),
child: Text(
'Actions',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFF555555),
),
),
),
),
],
),
),
// Grouped Enquiries
EnquiryGroup(
title: 'Awaiting Proposal',
icon: '🕐',
groupKey: 'awaiting',
enquiries: groupedEnquiries['awaiting']!,
onUpdate: _updateEnquiry,
partners: _partners,
insurers: _insurers,
assignees: _assignees,
),
EnquiryGroup(
title: 'Proposal Created',
icon: '📝',
groupKey: 'created',
enquiries: groupedEnquiries['created']!,
onUpdate: _updateEnquiry,
partners: _partners,
insurers: _insurers,
assignees: _assignees,
),
EnquiryGroup(
title: 'Proposal Accepted',
icon: '',
groupKey: 'accepted',
enquiries: groupedEnquiries['accepted']!,
onUpdate: _updateEnquiry,
partners: _partners,
insurers: _insurers,
assignees: _assignees,
),
EnquiryGroup(
title: 'Proposal Rejected',
icon: '',
groupKey: 'rejected',
enquiries: groupedEnquiries['rejected']!,
onUpdate: _updateEnquiry,
partners: _partners,
insurers: _insurers,
assignees: _assignees,
),
EnquiryGroup(
title: 'Policy Created',
icon: '📋',
groupKey: 'policy',
enquiries: groupedEnquiries['policy']!,
onUpdate: _updateEnquiry,
partners: _partners,
insurers: _insurers,
assignees: _assignees,
),
],
),
),
),
),
const SizedBox(height: 20),
],
),
);
}
}

View File

@ -0,0 +1,473 @@
// 5. Replace your existing _buildDataTable method with this grouped version
Widget _buildDataTable(BuildContext context) {
if (filteredData.isEmpty) {
return const SizedBox(
height: 50,
child: Center(child: Text('No available data')),
);
}
final isDesktop = !ResponsiveLayout.isMobile(context);
final groupedData = _groupedEnquiries;
return LayoutBuilder(
builder: (context, constraints) {
double minWidth = constraints.maxWidth < 1300 ? 1300 : constraints.maxWidth;
return ScrollConfiguration(
behavior: const MaterialScrollBehavior().copyWith(
dragDevices: {PointerDeviceKind.mouse, PointerDeviceKind.touch},
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: minWidth),
child: Column(
children: [
// Table Header (sticky)
Container(
color: Color(0xFFEDF6F5),
child: _buildTableHeader(isDesktop),
),
// Grouped Rows
...groupedData.entries.map((entry) {
final status = entry.key;
final items = entry.value;
final isExpanded = groupExpanded[status] ?? true;
return Column(
children: [
// Group Header
_buildGroupHeader(status, items.length),
// Group Rows (if expanded)
if (isExpanded)
...items.isEmpty
? [
Padding(
padding: EdgeInsets.all(20),
child: Text(
'No records found',
style: GoogleFonts.inter(
fontSize: 12,
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
),
]
: items.asMap().entries.map((itemEntry) {
final index = itemEntry.key;
final item = itemEntry.value;
final startIndex = (currentPage - 1) * itemsPerPage;
final sno = startIndex + index + 1;
return _buildGroupedDataRow(
item,
sno,
isDesktop,
);
}).toList(),
],
);
}).toList(),
],
),
),
),
),
);
},
);
}
// 6. Build table header
Widget _buildTableHeader(bool isDesktop) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
_buildHeaderCell('Received Date', 120),
_buildHeaderCell('Partner *', 110),
_buildHeaderCell('Assigned To *', 110),
_buildHeaderCell('Broker *', 100),
_buildHeaderCell('Insurer *', 110),
_buildHeaderCell('Insured Name *', 120),
_buildHeaderCell('Vehicle No *', 120),
_buildHeaderCell('Vehicle Type *', 110),
_buildHeaderCell('Email', 150),
_buildHeaderCell('Mobile', 100),
_buildHeaderCell('Documents', 100),
_buildHeaderCell('Remarks', 120),
if (roleId != 'staff') _buildHeaderCell('Action', 100),
_buildHeaderCell('Assigned Date', 120),
_buildHeaderCell('Premium', 100),
_buildHeaderCell('Payment Mode', 100),
_buildHeaderCell('Policy Number', 120),
_buildHeaderCell('Status', 100),
],
),
);
}
Widget _buildHeaderCell(String label, double width) {
return Container(
width: width,
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
);
}
// 7. Build grouped data row (container-based instead of DataTable)
Widget _buildGroupedDataRow(
Map<String, dynamic> item,
int sno,
bool isDesktop,
) {
final id = int.tryParse(item['id'].toString()) ?? 0;
final selectId = item['id'].toString() ?? '0';
final editing = isEditingRow[id] ?? false;
final bool isNewRow =
int.tryParse(id.toString()) != null &&
int.parse(id.toString()) > 1000000000000;
return Container(
decoration: BoxDecoration(
color: editing ? Color(0xFFf8f9fa) : Colors.white,
border: Border(
bottom: BorderSide(color: Colors.grey.shade200, width: 0.5),
),
),
child: InkWell(
onTap: () {
// Optional: handle row tap
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
// Received Date
_buildDataCell(
formatDateTimeForTable(item['created_on'], newLine: true),
120,
),
// Partner
_buildDataCell(
editing ? null : item['agent_name'] ?? '-',
110,
widget: editing ? buildAgentName(context, fromHeader: false) : null,
),
// Assigned To
_buildDataCell(
editing ? null : item['assigned_to_name'] ?? '-',
110,
widget: editing ? buildSelectStaffMem(context) : null,
subtitle: editing ? null : item['agent_code'],
),
// Broker
_buildDataCell(
editing ? null : item['broker_name'] ?? '-',
100,
widget: editing ? buildBroker(context) : null,
),
// Insurer
_buildDataCell(
editing ? null : item['insurer_short_name'] ?? '-',
110,
widget: editing ? buildInsurer(context, fromHeader: false) : null,
),
// Insured Name
_buildDataCell(
editing ? null : item['insured_name'] ?? '-',
120,
widget: editing ? buildInsuredName(context, fromHeader: false) : null,
),
// Vehicle No
_buildDataCell(
editing ? null : item['reg_no'] ?? '-',
120,
widget: editing ? buildVehicleNumber(context) : null,
),
// Vehicle Type
_buildDataCell(
editing ? null : _wrapText(item['vehicle_type'] ?? '-', 13),
110,
widget: editing ? buildVehicleType(context) : null,
),
// Email
_buildDataCell(
editing ? null : item['email'] ?? '-',
150,
widget: editing ? buildEmail(context) : null,
),
// Mobile
_buildDataCell(
editing ? null : item['mobile'] ?? '-',
100,
widget: editing ? buildPhNumber(context) : null,
),
// Documents
_buildDocumentCell(selectId, editing, 100),
// Remarks
_buildDataCell(
editing ? null : _truncateText(item['remarks'] ?? '-', 10),
120,
widget: editing ? buildRemarks(context) : null,
tooltip: item['remarks'],
),
// Action (if not staff)
if (roleId != 'staff')
_buildActionCell(item, id, editing, isNewRow, 100),
// Assigned Date
_buildDataCell(
formatDateTimeForTable(item['updated_on'], newLine: true),
120,
),
// Premium
_buildPremiumCell(item, 100),
// Payment Mode
_buildDataCell(item['payment_mode'] ?? '-', 100),
// Policy Number
_buildDataCell(item['policy_number'] ?? '-', 120),
// Status
_buildStatusCell(item, 100),
],
),
),
),
);
}
// 8. Helper widget builders
Widget _buildDataCell(
String? text,
double width, {
Widget? widget,
String? subtitle,
String? tooltip,
}) {
if (widget != null) {
return Container(
width: width,
padding: EdgeInsets.symmetric(horizontal: 4),
child: widget,
);
}
Widget child = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
text ?? '-',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
if (subtitle != null)
Text(
subtitle,
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w400,
color: Colors.grey,
),
),
],
);
if (tooltip != null && tooltip.isNotEmpty) {
child = Tooltip(
message: tooltip,
waitDuration: Duration(milliseconds: 400),
child: child,
);
}
return Container(
width: width,
padding: EdgeInsets.symmetric(horizontal: 4),
child: child,
);
}
Widget _buildDocumentCell(String selectId, bool editing, double width) {
return Container(
width: width,
padding: EdgeInsets.symmetric(horizontal: 4),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
final RenderBox button = context.findRenderObject() as RenderBox;
final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
final Offset position = button.localToGlobal(Offset.zero, ancestor: overlay);
await showMenu(
context: context,
position: RelativeRect.fromLTRB(
position.dx,
position.dy + button.size.height,
overlay.size.width,
0,
),
items: [
PopupMenuItem(
child: Column(
mainAxisSize: MainAxisSize.min,
children: _buildPopupDownload(context, selectId, editing),
),
),
],
color: Colors.white,
);
},
child: Tooltip(
message: editing ? 'Click To Upload Documents' : 'Click To View Documents',
child: Icon(
editing ? Icons.upload_file : Icons.download_for_offline,
color: editing ? Colors.blue : Colors.green,
size: 20,
),
),
),
),
);
}
Widget _buildActionCell(
Map<String, dynamic> item,
int id,
bool editing,
bool isNewRow,
double width,
) {
return Container(
width: width,
padding: EdgeInsets.symmetric(horizontal: 4),
child: Row(
children: [
if (editing) ...[
IconButton(
icon: Icon(Icons.check, color: Colors.green, size: 18),
tooltip: "Save",
onPressed: () => handleSave(id),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
),
SizedBox(width: 4),
IconButton(
icon: Icon(
isNewRow ? Icons.close : Icons.edit_off_sharp,
color: isNewRow ? Colors.red : Colors.blue,
size: 18,
),
tooltip: isNewRow ? "Delete" : "Cancel Edit Mode",
onPressed: () => handleCancel(id),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
),
] else ...[
IconButton(
icon: Icon(Icons.edit, color: Colors.blue, size: 18),
tooltip: "Edit",
onPressed: () => handleEdit(item),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
),
],
],
),
);
}
Widget _buildPremiumCell(Map<String, dynamic> item, double width) {
final showApproval = item['status'] == 'Proposal Created' ||
item['status'] == 'Proposal Rejected';
return Container(
width: width,
padding: EdgeInsets.symmetric(horizontal: 4),
child: showApproval
? InkWell(
onTap: () => handleProposalAccept(context, item['id']),
child: Text(
'Click To\nApprove',
style: GoogleFonts.inter(
color: Colors.green,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
)
: Text(
item['premium_amount']?.toString() ?? '-',
style: GoogleFonts.inter(fontSize: 12, fontWeight: FontWeight.w500),
),
);
}
Widget _buildStatusCell(Map<String, dynamic> item, double width) {
return Container(
width: width,
padding: EdgeInsets.symmetric(horizontal: 4),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
var status = item['status'];
var enqId = item['id'];
final prefs = await SharedPreferences.getInstance();
await prefs.remove('enqStaffDataId');
await prefs.setString('enqStaffDataId', enqId.toString());
ref.read(quotationStaffIdProvider.notifier).state = enqId;
buildStatusActions(context, status, enqId);
},
child: Tooltip(
message: 'Click to View or Process Enquiry',
waitDuration: Duration(milliseconds: 500),
child: Text(
item['status'] ?? '-',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
),
),
);
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:dropdown_search/dropdown_search.dart';
import '../model/dropdown_option.dart';
class SearchableDropdown extends StatelessWidget {
final String label;
final String? value;
final List<DropdownOption> items;
final Function(String?) onChanged;
final String? Function(String?)? validator;
final bool enabled;
const SearchableDropdown({
super.key,
required this.label,
required this.value,
required this.items,
required this.onChanged,
this.validator,
this.enabled = true,
});
@override
Widget build(BuildContext context) {
return DropdownSearch<DropdownOption>(
// items: items,
selectedItem: items.where((item) => item.value == value).firstOrNull,
itemAsString: (item) => item.label,
onChanged: (item) => onChanged(item?.value),
enabled: enabled,
// dropdownDecoratorProps: DropDownDecoratorProps(
// dropdownSearchDecoration: InputDecoration(
// labelText: label,
// border: const OutlineInputBorder(),
// contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
// ),
// ),
popupProps: PopupProps.menu(
showSearchBox: true,
searchFieldProps: const TextFieldProps(
decoration: InputDecoration(
hintText: 'Search...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
),
),
// itemBuilder: (context, item, isSelected) {
// return Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
// decoration: BoxDecoration(
// color: isSelected ? const Color(0xFFe3f2fd) : null,
// ),
// child: Text(
// item.label,
// style: TextStyle(
// fontSize: 13,
// fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
// ),
// ),
// );
// },
menuProps: const MenuProps(
backgroundColor: Colors.white,
elevation: 8,
borderRadius: BorderRadius.all(Radius.circular(4)),
),
searchDelay: const Duration(milliseconds: 300),
emptyBuilder: (context, searchEntry) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
'No items found',
style: TextStyle(color: Colors.grey, fontSize: 13),
),
),
);
},
),
compareFn: (item1, item2) => item1.value == item2.value,
// validator: validator,
dropdownBuilder: (context, selectedItem) {
return Text(
selectedItem?.label ?? '',
style: const TextStyle(fontSize: 13),
);
},
);
}
}

View File

@ -132,6 +132,10 @@ class _ThemedUploadFieldState extends State<ThemedUploadField> {
),
),
),
// const Icon(
// Icons.file_upload_outlined,
// color: Colors.black,
// ),
],
],
),

View File

@ -85,7 +85,7 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
width: widget.txtwidth ?? MediaQuery.of(context).size.width,
height: widget.txtheight,
padding: const EdgeInsets.symmetric(
vertical: 10,
vertical: 8,
horizontal: 15,
),
decoration: BoxDecoration(
@ -94,7 +94,7 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
border: Border.all(
color: hasError
? (widget.errorBorderColor ?? Color(0xFFD83731))
: (widget.borderColor ?? Colors.grey.shade50),
: (widget.borderColor ?? Color(0xFFE2E8F0)),
width: 1,
),
),
@ -107,7 +107,7 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
? widget.controller!.text
: widget.hintText ?? "Select Date",
style: TextStyle(
fontSize: 13,
fontSize: 11,
color: hasError ? Color(0xFFD83731) : Colors.black,
),
overflow: TextOverflow.ellipsis,
@ -116,7 +116,8 @@ class _ThemedDateFieldState extends State<ThemedDateField> {
const Icon(
Icons.calendar_today,
size: 16,
color: Colors.black,
color: Color(0xFF5A6C7D),
),
],
),

View File

@ -74,7 +74,8 @@ class ExportBtn extends HookWidget {
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: const Color(0xFF425B5B),
color: const Color(0xFF2E7D6E),
// color: const Color(0xFF425B5B),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(

View File

@ -4,7 +4,7 @@ class AppInputDecorations {
static InputDecoration dropdownDecoration({String? label, String? hint}) {
return InputDecoration(
labelText: label,
labelStyle: TextStyle(fontSize: 12),
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
hintText: hint,
filled: true,
fillColor: const Color(0xFFECECEC),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
// import 'package:uae_stat/config/my_theme.dart';
@ -55,20 +56,23 @@ class ThemedSearchField extends HookWidget {
// contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 15),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFFF6F8F8)),
borderSide: BorderSide(color: Color(0xFFE2E8F0)),
// borderSide: BorderSide(color: Color(0xFFF6F8F8)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFFF6F8F8)),
// borderSide: BorderSide(color: Color(0xFFF6F8F8)),
borderSide: BorderSide(color: Color(0xFFE2E8F0)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Color(0xFF50A398), width: 2),
),
hintText: hintText,
hintStyle: TextStyle(
color: const Color(0xFF686868),
fontSize: (txtHeight ?? 45) <= 45 ? 14 : 16,
hintStyle: GoogleFonts.inter(
// color: const Color(0xFF686868),
color: const Color(0xFF94A3B8),
fontSize: (txtHeight ?? 45) <= 45 ? 12 : 14,
),
// suffixIconConstraints: const BoxConstraints(
// maxWidth: 25 + 16 + 10,
@ -76,7 +80,8 @@ class ThemedSearchField extends HookWidget {
// ),
suffixIcon: Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 10),
child: Icon(Icons.search, color: const Color(0xFF686868), size: 20),
child: Icon(Icons.search, color: const Color(0xFF94A3B8), size: 18),
// child: Icon(Icons.search, color: const Color(0xFF686868), size: 20),
),
);

View File

@ -31,12 +31,14 @@ class ThemedFormField extends HookWidget {
this.verticalPad,
this.horizonalPad,
this.enableBorderWidth,
this.textColr,
});
final double? verticalPad;
final double? horizonalPad;
final double? enableBorderWidth;
final String? hintText;
final Color? textColr;
final String? imgPath;
final String? Function(String? text)? validator;
@ -130,7 +132,10 @@ class ThemedFormField extends HookWidget {
)
: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Color(0xFF50A398), width: 2),
borderSide: BorderSide(
color: borderColor ?? Color(0xFF50A398),
width: 2,
),
),
hintText: hintText,
hintStyle: TextStyle(
@ -187,7 +192,7 @@ class ThemedFormField extends HookWidget {
style: GoogleFonts.inter(
fontSize: 12, // 👈 Change this to your desired size
fontWeight: FontWeight.w400, // optional
color: Colors.black, // optional
color: textColr ?? Colors.black, // optional
),
),
),

View File

@ -18,6 +18,7 @@ class ThemedFormInlineField extends HookWidget {
this.isObscurable = false,
this.hintColor,
this.backgroundColor,
this.textColr,
this.borderColor,
this.errorBorderColor,
this.errorTextColor,
@ -41,6 +42,7 @@ class ThemedFormInlineField extends HookWidget {
final Color? borderColor;
final Color? errorBorderColor;
final Color? errorTextColor;
final Color? textColr;
final double? txtwidth;
final bool readOnly;
final bool? isdense;
@ -96,14 +98,14 @@ class ThemedFormInlineField extends HookWidget {
filled: true,
fillColor: backgroundColor ?? Colors.white,
// contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 2),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
// borderSide: BorderSide(color: Color(0xFF50A398)),
borderSide: BorderSide(
color: borderColor ?? Color(0xFFFFFFFF),
width: 0.1,
width: 0.5,
),
),
enabledBorder: OutlineInputBorder(
@ -111,7 +113,7 @@ class ThemedFormInlineField extends HookWidget {
// borderSide: BorderSide(color: Color(0xFF50A398)),
borderSide: BorderSide(
color: borderColor ?? backgroundColor ?? Color(0xFFFFFFFF),
width: 0.1,
width: 0.5,
),
),
focusedBorder: readOnly
@ -122,7 +124,10 @@ class ThemedFormInlineField extends HookWidget {
)
: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Color(0xFF50A398), width: 2),
borderSide: BorderSide(
color: borderColor ?? Color(0xFF50A398),
width: 2,
),
),
hintText: hintText,
hintStyle: TextStyle(
@ -177,7 +182,7 @@ class ThemedFormInlineField extends HookWidget {
style: GoogleFonts.inter(
fontSize: 12, // 👈 Change this to your desired size
fontWeight: FontWeight.w400, // optional
color: Colors.black, // optional
color: textColr ?? Colors.black, // optional
),
),
),

View File

@ -196,7 +196,12 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
child: Tooltip(
message: 'Filter',
child: IconButton(
icon: const Icon(Icons.filter_alt_outlined),
icon: const Icon(
Icons.filter_alt_outlined,
size: 18,
color: const Color(0xFF94A3B8),
),
onPressed: () {
if (widget.formKey.currentState!.validate()) {
widget.onFilter();
@ -212,7 +217,11 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
message: 'Refresh',
child: IconButton(
onPressed: widget.onRefresh,
icon: const Icon(Icons.refresh),
icon: const Icon(
Icons.refresh,
size: 18,
color: const Color(0xFF94A3B8),
),
),
),
),
@ -251,20 +260,25 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
children: [
// Text('Start Date', style: _textStyle),
// SizedBox(height: 5),
ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.1,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
// validator: (value) => Validators.requiredField(value, "date"),
controller: widget.startController,
onDateSelected: (date) {
print("Picked Date: $date");
widget.startController.text = DateFormat('dd-MM-yyyy').format(date);
// controllers['date']?.text = date as String;
},
SizedBox(
height: 35,
child: ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.1,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
// validator: (value) => Validators.requiredField(value, "date"),
controller: widget.startController,
onDateSelected: (date) {
print("Picked Date: $date");
widget.startController.text = DateFormat(
'dd-MM-yyyy',
).format(date);
// controllers['date']?.text = date as String;
},
),
),
],
);
@ -276,35 +290,38 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
children: [
// Text('End Date', style: _textStyle),
// SizedBox(height: 5),
ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.1,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
validator: (value) {
final fromText = widget.startController.text ?? '';
if (fromText.isNotEmpty) {
final startDate = DateFormat('dd-MM-yyyy').parse(fromText);
if (value == null || value.isEmpty) {
return "End Date is required";
}
final endDate = DateFormat('dd-MM-yyyy').parse(value);
SizedBox(
height: 35,
child: ThemedDateField(
hintText: "Select Date",
txtwidth: ResponsiveLayout.isMobile(context)
? null
: MediaQuery.of(context).size.width * 0.1,
// txtheight: 50,
// backgroundColor: const Color(0xFFECECEC),
validator: (value) {
final fromText = widget.startController.text ?? '';
if (fromText.isNotEmpty) {
final startDate = DateFormat('dd-MM-yyyy').parse(fromText);
if (value == null || value.isEmpty) {
return "End Date is required";
}
final endDate = DateFormat('dd-MM-yyyy').parse(value);
if (endDate.isBefore(startDate)) {
return "End Date cannot be earlier than Start Date";
if (endDate.isBefore(startDate)) {
return "End Date cannot be earlier than Start Date";
}
}
}
return null; // no error
},
controller: widget.endController,
lastDate: DateTime.now(),
onDateSelected: (date) {
print("Picked Date: $date");
widget.endController.text = DateFormat('dd-MM-yyyy').format(date);
// controllers['date']?.text = date as String;
},
return null; // no error
},
controller: widget.endController,
lastDate: DateTime.now(),
onDateSelected: (date) {
print("Picked Date: $date");
widget.endController.text = DateFormat('dd-MM-yyyy').format(date);
// controllers['date']?.text = date as String;
},
),
),
],
);
@ -339,11 +356,11 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
// Text('Status', style: _textStyle),
// SizedBox(height: 5),
SizedBox(
height: 40,
height: 35,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(color: Colors.grey.shade100),
border: Border.all(color: Color(0xFFE2E8F0)),
// color: Colors.black,
),
width: ResponsiveLayout.isMobile(context)
@ -390,7 +407,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
isDense: true, // 👈 reduces built-in vertical padding
contentPadding: const EdgeInsets.symmetric(
horizontal: 2,
horizontal: 6,
vertical: 1, // 👈 adjust this to make the field shorter
),
),
@ -480,11 +497,11 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
// Text("Staff Name", style: _textStyle),
// SizedBox(height: 10),
SizedBox(
height: 40,
height: 35,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(color: Colors.grey.shade100),
border: Border.all(color: Color(0xFFE2E8F0)),
// color: Colors.black,
),
@ -524,7 +541,7 @@ class _DateFilterRowState extends ConsumerState<DateFilterRow> {
fillColor:
Colors.white, // 👈 makes the dropdown input white
contentPadding: const EdgeInsets.symmetric(
horizontal: 2,
horizontal: 6,
vertical: 1, // 👈 adjust this to make the field shorter
),
),

View File

@ -137,9 +137,11 @@ class DrawerMenuState extends ConsumerState<DrawerMenu> {
_hidePopup();
if (roleId == 'agent') {
context.go(AppRoutes.enquiryLst);
} else if (roleId == 'handler') {
context.go(AppRoutes.enquiryHandlerLst);
} else {
}
// else if (roleId == 'handler') {
// context.go(AppRoutes.enquiryHandlerLst);
// }
else {
context.go(AppRoutes.enquiryForStaff);
}
},