diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index 4f775d6..c0032e0 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging-16.0.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/home/venba/.pub-cache/hosted/pub.dev/fluttertoast-9.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.2.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_darwin","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_darwin-1.6.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_ios-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"syncfusion_flutter_pdfviewer","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_flutter_pdfviewer-31.1.19/","native_build":true,"dependencies":["device_info_plus"],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging-16.0.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.31/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/home/venba/.pub-cache/hosted/pub.dev/fluttertoast-9.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_android","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_android-7.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_android","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_android-1.0.52/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"open_file_android","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_android-1.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_android-2.2.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.13/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"syncfusion_flutter_pdfviewer","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_flutter_pdfviewer-31.1.19/","native_build":true,"dependencies":["device_info_plus"],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.20/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging-16.0.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_secure_storage_macos","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.2.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_darwin","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_darwin-1.6.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_mac","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_mac-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"syncfusion_pdfviewer_macos","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_macos-31.1.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_linux-0.0.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"syncfusion_pdfviewer_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_linux-31.1.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_windows-1.0.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_windows-0.0.3/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"syncfusion_pdfviewer_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_windows-31.1.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","dependencies":[],"dev_dependency":false},{"name":"firebase_auth_web","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth_web-6.1.0/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"firebase_core_web","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core_web-3.3.0/","dependencies":[],"dev_dependency":false},{"name":"firebase_messaging_web","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging_web-4.1.0/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"flutter_secure_storage_web","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/home/venba/.pub-cache/hosted/pub.dev/fluttertoast-9.0.0/","dependencies":[],"dev_dependency":false},{"name":"google_sign_in_web","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_web-1.1.0/","dependencies":[],"dev_dependency":false},{"name":"open_file_web","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_web-0.0.4/","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"syncfusion_pdfviewer_web","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_web-31.1.19/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_messaging","dependencies":["firebase_core","firebase_messaging_web"]},{"name":"firebase_messaging_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"fluttertoast","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"local_auth","dependencies":["local_auth_android","local_auth_darwin","local_auth_windows"]},{"name":"local_auth_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"local_auth_darwin","dependencies":[]},{"name":"local_auth_windows","dependencies":[]},{"name":"open_file","dependencies":["open_file_android","open_file_web","open_file_ios","open_file_mac","open_file_windows","open_file_linux"]},{"name":"open_file_android","dependencies":[]},{"name":"open_file_ios","dependencies":[]},{"name":"open_file_linux","dependencies":[]},{"name":"open_file_mac","dependencies":[]},{"name":"open_file_web","dependencies":[]},{"name":"open_file_windows","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"pdf_render","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"syncfusion_flutter_pdfviewer","dependencies":["url_launcher","device_info_plus","syncfusion_pdfviewer_web","syncfusion_pdfviewer_macos","syncfusion_pdfviewer_windows","syncfusion_pdfviewer_linux"]},{"name":"syncfusion_pdfviewer_linux","dependencies":[]},{"name":"syncfusion_pdfviewer_macos","dependencies":[]},{"name":"syncfusion_pdfviewer_web","dependencies":[]},{"name":"syncfusion_pdfviewer_windows","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-03-16 18:55:57.770936","version":"3.41.4","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging-16.0.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/home/venba/.pub-cache/hosted/pub.dev/fluttertoast-9.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.2.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_darwin","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_darwin-1.6.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_ios-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"syncfusion_flutter_pdfviewer","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_flutter_pdfviewer-31.1.19/","native_build":true,"dependencies":["device_info_plus"],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging-16.0.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.31/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/home/venba/.pub-cache/hosted/pub.dev/fluttertoast-9.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_android","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_android-7.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_android","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_android-1.0.52/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"open_file_android","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_android-1.0.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_android-2.2.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.13/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"syncfusion_flutter_pdfviewer","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_flutter_pdfviewer-31.1.19/","native_build":true,"dependencies":["device_info_plus"],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.20/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging-16.0.4/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_secure_storage_macos","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.2.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_darwin","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_darwin-1.6.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_mac","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_mac-1.0.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"syncfusion_pdfviewer_macos","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_macos-31.1.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_linux-0.0.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"syncfusion_pdfviewer_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_linux-31.1.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth-6.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core-4.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_secure_storage_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"local_auth_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/local_auth_windows-1.0.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"open_file_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_windows-0.0.3/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"syncfusion_pdfviewer_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_windows-31.1.19/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/device_info_plus-11.5.0/","dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/venba/.pub-cache/hosted/pub.dev/file_picker-10.3.7/","dependencies":[],"dev_dependency":false},{"name":"firebase_auth_web","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_auth_web-6.1.0/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"firebase_core_web","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_core_web-3.3.0/","dependencies":[],"dev_dependency":false},{"name":"firebase_messaging_web","path":"/home/venba/.pub-cache/hosted/pub.dev/firebase_messaging_web-4.1.0/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"flutter_secure_storage_web","path":"/home/venba/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/home/venba/.pub-cache/hosted/pub.dev/fluttertoast-9.0.0/","dependencies":[],"dev_dependency":false},{"name":"google_sign_in_web","path":"/home/venba/.pub-cache/hosted/pub.dev/google_sign_in_web-1.1.0/","dependencies":[],"dev_dependency":false},{"name":"open_file_web","path":"/home/venba/.pub-cache/hosted/pub.dev/open_file_web-0.0.4/","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/venba/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","dependencies":[],"dev_dependency":false},{"name":"pdf_render","path":"/home/venba/.pub-cache/hosted/pub.dev/pdf_render-1.4.12/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/venba/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"syncfusion_pdfviewer_web","path":"/home/venba/.pub-cache/hosted/pub.dev/syncfusion_pdfviewer_web-31.1.19/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/venba/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_messaging","dependencies":["firebase_core","firebase_messaging_web"]},{"name":"firebase_messaging_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"fluttertoast","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"local_auth","dependencies":["local_auth_android","local_auth_darwin","local_auth_windows"]},{"name":"local_auth_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"local_auth_darwin","dependencies":[]},{"name":"local_auth_windows","dependencies":[]},{"name":"open_file","dependencies":["open_file_android","open_file_web","open_file_ios","open_file_mac","open_file_windows","open_file_linux"]},{"name":"open_file_android","dependencies":[]},{"name":"open_file_ios","dependencies":[]},{"name":"open_file_linux","dependencies":[]},{"name":"open_file_mac","dependencies":[]},{"name":"open_file_web","dependencies":[]},{"name":"open_file_windows","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"pdf_render","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"syncfusion_flutter_pdfviewer","dependencies":["url_launcher","device_info_plus","syncfusion_pdfviewer_web","syncfusion_pdfviewer_macos","syncfusion_pdfviewer_windows","syncfusion_pdfviewer_linux"]},{"name":"syncfusion_pdfviewer_linux","dependencies":[]},{"name":"syncfusion_pdfviewer_macos","dependencies":[]},{"name":"syncfusion_pdfviewer_web","dependencies":[]},{"name":"syncfusion_pdfviewer_windows","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-03-31 18:32:27.151243","version":"3.41.4","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index 2954141..91e3731 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -7,9 +7,11 @@ import 'package:nhance_partner/presentation/screens/Masters/PaymentMode/paymentL import 'package:nhance_partner/presentation/screens/UserManagement/Profile/profile_web.dart'; import 'package:nhance_partner/presentation/screens/dashboard/dashboard_nw.dart'; import 'package:nhance_partner/presentation/screens/payout/invoice_list.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../data/services/auth_service.dart'; import '../../presentation/providers/manager_provider.dart'; +import '../../presentation/providers/userRoleProvider.dart'; import '../../presentation/screens/Enquiry/enquiry/tabs.dart'; import '../../presentation/screens/Enquiry/policy_claims_endros/claims.dart'; import '../../presentation/screens/Enquiry/policy_claims_endros/endorsomentUpdateValidation.dart'; @@ -33,6 +35,8 @@ import '../../presentation/screens/handler/enquiryListOld.dart'; import '../../presentation/screens/home/home_screen.dart'; import '../../presentation/screens/login/login_screen.dart'; import '../../presentation/screens/payout/payout_screen.dart'; +import '../../presentation/screens/payout/payout_details.dart'; +import '../../presentation/screens/payout/payout_list.dart'; import '../../presentation/screens/splash/splash_screen.dart'; import '../../presentation/screens/staff/Enquiry/enquiry_inline_Edit_17Nov.dart'; import '../../presentation/screens/staff/Enquiry/enquiry_inline_list.dart'; @@ -42,7 +46,11 @@ import '../../presentation/screens/staff/policy/policy.dart'; import '../../presentation/screens/staff/policy/policy_list.dart'; import '../../presentation/screens/staff/policy/policy_validation.dart'; import '../../presentation/screens/staff/quotations/quotation.dart'; +import '../../presentation/screens/Grid/gridList.dart'; +import '../../presentation/screens/Grid/gridUpload.dart'; +import '../../presentation/screens/Grid/gridView.dart'; import '../config/navigation_service.dart'; +import '../../presentation/screens/dashboard/partner_portal_dashboard.dart'; final GoRouter appRouter = GoRouter( navigatorKey: navigatorKey, @@ -57,13 +65,17 @@ final GoRouter appRouter = GoRouter( path: AppRoutes.login, builder: (context, state) => const LoginScreen(), ), + // GoRoute( // path: AppRoutes.home, // builder: (context, state) => const HomeScreen(), // ), + GoRoute( + path: AppRoutes.partnerPortalDashboard, + builder: (context, state) => const PartnerPortalDashboard(), + ), GoRoute( path: AppRoutes.dashboard, - // builder: (context, state) => const DashboardScreen(), builder: (context, state) => const Dashboard(), ), @@ -79,6 +91,17 @@ final GoRouter appRouter = GoRouter( path: AppRoutes.invoiceList, builder: (context, state) => const InvoiceList(), ), + GoRoute( + path: AppRoutes.payoutList, + builder: (context, state) => const PayoutList(), + ), + GoRoute( + path: AppRoutes.payoutDetails, + builder: (context, state) { + final editItem = state.extra as Map?; + return PayOutDetails(editItem: editItem); + }, + ), GoRoute( path: AppRoutes.profile, @@ -236,7 +259,6 @@ final GoRouter appRouter = GoRouter( path: AppRoutes.endorsomentValidation, name: "endorsomentValidation", pageBuilder: (context, state) { - final extra = state.extra as Map; return CustomTransitionPage( @@ -293,7 +315,7 @@ final GoRouter appRouter = GoRouter( builder: (context, state) => PolicyStaffEnqList(), ), - GoRoute( + GoRoute( path: AppRoutes.enquiryForStaff, builder: (context, state) => const EnquiryListStaffInline(), // builder: (context, state) => const EnquiryListStaffInlineTST(), @@ -331,6 +353,18 @@ final GoRouter appRouter = GoRouter( path: AppRoutes.paymentModeLst, builder: (context, state) => const PaymentLsit(), ), + GoRoute( + path: AppRoutes.gridList, + builder: (context, state) => const GridListScreen(), + ), + GoRoute( + path: AppRoutes.gridUpload, + builder: (context, state) => const GridUploadScreen(), + ), + GoRoute( + path: AppRoutes.gridView, + builder: (context, state) => const GridViewScreen(), + ), ], redirect: (context, state) async { final loggedIn = await AuthService.isLoggedIn(); @@ -351,9 +385,30 @@ final GoRouter appRouter = GoRouter( } if (loggedIn && goingToLogin) { - // Logged in but going to login → send to "next" if present + // Logged in but opening login → continue to intended route or dashboard final next = state.uri.queryParameters['next']; - return next ?? AppRoutes.login; + final prefs = await SharedPreferences.getInstance(); + final role = prefs.getString('userRole'); + + if (next != null && next.isNotEmpty) { + if (role == 'agent' && next == AppRoutes.dashboard) { + return AppRoutes.partnerPortalDashboard; + } + return next; + } + + if (role == 'staff') return AppRoutes.enquiryForStaff; + //if (role == 'Accounts') return AppRoutes.invoiceList; + if (role == 'Accounts') return AppRoutes.payoutList; + if (role == 'agent') return AppRoutes.partnerPortalDashboard; + return AppRoutes.dashboard; + } + + // Hard guard: agent must never land on manager dashboard route. + if (loggedIn && currentLocation == AppRoutes.dashboard) { + final prefs = await SharedPreferences.getInstance(); + final role = prefs.getString('userRole'); + if (role == 'agent') return AppRoutes.partnerPortalDashboard; } // if (!loggedIn && !goingToLogin) return AppRoutes.login; diff --git a/lib/core/routing/routes.dart b/lib/core/routing/routes.dart index 7244ffb..f140743 100644 --- a/lib/core/routing/routes.dart +++ b/lib/core/routing/routes.dart @@ -1,7 +1,23 @@ class AppRoutes { + // --------------------------------------------------------------------------- + // Post-login routing (JWT role_id → userRoleProvider → first screen) + // --------------------------------------------------------------------------- + // | role_id | userRoleProvider | First route | Screen / notes | + // |---------|------------------|--------------------------|-----------------------| + // | 1 | manager | /dashboard | Manager Dashboard | + // | 2 | handler | /enquiryHandlerLst | Handler enquiry list | + // | 3 | staff | /enquiryForStaff | Staff inline enquiry | + // | 4 | Accounts | /enquiryLst | Enquiry page | + // | (else) | agent | /partnerPortalDashboard | Partner portal | + // Partner “Login as Partner” forces role `agent` → partner route above. | + // --------------------------------------------------------------------------- + + static const String splash = '/'; static const String home = '/home'; static const String dashboard = '/dashboard'; + /// Partner (agent) home — shows `PartnerPortalDashboard`. + static const String partnerPortalDashboard = '/partnerPortalDashboard'; static const String login = '/login'; static const String profile = '/profile'; static const String agentLst = '/agentLst'; @@ -34,4 +50,10 @@ class AppRoutes { static const String paymentModeLst = '/paymentMode'; static const String endorsementTypeLst = '/endorsementTypeLst'; static const String vehicleTypeLst = '/vehicleTypeLst'; + static const String payoutList = '/payoutList'; + static const String payoutDetails = '/payoutDetails'; + static const String payoutGrid = '/payoutGrid'; + static const String gridList = '/gridList'; + static const String gridUpload = '/gridUpload'; + static const String gridView = '/gridView'; } diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index 1aea736..96b2252 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -57,9 +57,9 @@ class ApiService { // } Future> _makeGetRequest( - Uri url, - Map headers, - ) async { + Uri url, + Map headers, + ) async { final response = await http.get(url, headers: headers); return _handleResponse(response); } @@ -431,9 +431,9 @@ class ApiService { String path, String id, String month, - dynamic managerId, - {DateTime? toDate, DateTime? fromDate} - ) async { + dynamic managerId, + {DateTime? toDate, DateTime? fromDate} + ) async { print('Sq getxl 1 - $path'); print('Sq toDate - $toDate'); print('Sq fromDate - $fromDate'); @@ -467,18 +467,18 @@ class ApiService { if (path == 'Insurer') { pathVal = - 'dashboard/downloadExcelInsurer?manager_id=$managerId&month=$month&insurer_id=$id'; + 'dashboard/downloadExcelInsurer?manager_id=$managerId&month=$month&insurer_id=$id'; } else if (path == 'Broker') { pathVal = - 'dashboard/downloadExcelBroker?manager_id=$managerId&month=$month&broker_id=$id'; + 'dashboard/downloadExcelBroker?manager_id=$managerId&month=$month&broker_id=$id'; } else if (path == 'Product') { pathVal = - 'dashboard/downloadExcelProduct?manager_id=$managerId&month=$month&vehicle_type=$id'; + 'dashboard/downloadExcelProduct?manager_id=$managerId&month=$month&vehicle_type=$id'; } else if (path == 'PerformingTop50') { pathVal = 'dashboard/downloadExcelPerformingAgentsTop50?manager_id=$managerId'; } else if (path == 'PerformingAgentsByID') { pathVal = - 'dashboard/downloadExcelPerformingAgentsByID?manager_id=$managerId&agent_id=$id'; + 'dashboard/downloadExcelPerformingAgentsByID?manager_id=$managerId&agent_id=$id'; } else if (path == 'NonPerformingBelow50K') { pathVal = 'dashboard/downloadExcelLowPremium?manager_id=$managerId'; } else if (path == 'staff_pending_summary') { @@ -487,7 +487,7 @@ class ApiService { pathVal = 'dashboard/downloadExcelStaffPendingSummaryByID?manager_id=$managerId&staff_id=$id&status=$month'; } else { //NoBusiness - pathVal = + pathVal = 'dashboard/downloadExcelWithoutPolicies?manager_id=$managerId'; } @@ -728,13 +728,13 @@ class ApiService { if (masterName == 'Broker') { url = Uri.parse('${Env.apiUrl}master/updateBrokerStatus/$id'); - } + } else if (masterName == 'EndorsementType') { url = Uri.parse('${Env.apiUrl}master/updateEndorsementStatus/$id'); - } + } else if (masterName == 'VehicleType') { url = Uri.parse('${Env.apiUrl}master/updateVehicleTypeStatus/$id'); - } + } else { url = Uri.parse('${Env.apiUrl}master/updatePaymentModeStatus/$id'); } @@ -767,7 +767,7 @@ class ApiService { final url; - url = Uri.parse('${Env.apiUrl}dashboard/managerDashboard?manager_id=$id&staff_id=$userId'); + url = Uri.parse('${Env.apiUrl}dashboard/managerDashboard?manager_id=$id&staff_id=$userId'); final headers = { 'Authorization': 'Bearer $_token' ?? '', @@ -837,7 +837,7 @@ class ApiService { if (fromDate == null || toDate == null) { url = Uri.parse( '${Env.apiUrl}dashboard/partnerDashboard?manager_id=$id', - ); + ); } /// ✅ CASE 2: both dates present → filtered API else { @@ -846,9 +846,9 @@ class ApiService { url = Uri.parse( '${Env.apiUrl}dashboard/partnerDashboard' - '?manager_id=$id' - '&from_date=$from' - '&to_date=$to', + '?manager_id=$id' + '&from_date=$from' + '&to_date=$to', ); } @@ -879,17 +879,19 @@ class ApiService { // -------------------------------- AGENT ---------------------------------------------- - Future> fetchAgentIncentiveList(agentId) async { + Future> fetchAgentIncentiveList( + dynamic agentId, { + String type = 'incentive', + }) async { // print(_token); if (_token == null) { await _initializeToken(); } final url = Uri.parse( - '${Env.apiUrl}agent/agentIncentiveFileList?agent_id=${agentId}', + '${Env.apiUrl}agent/agentIncentiveFileList?agent_id=$agentId&type=$type', ); - final headers = { 'Authorization': 'Bearer $_token' ?? '', 'app-signature': Env.App_Signature, @@ -898,6 +900,111 @@ class ApiService { return response; } + Future> fetchGridFileList() async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}grid/fileList'); + final headers = { + 'Authorization': 'Bearer $_token' ?? '', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + // ----------------------------------- PAYOUT GRID ---------------------------------------------- + Future> loadPayoutGrid({String? role, String? fileId}) async { + if (_token == null) { + await _initializeToken(); + } + + final roleTrimmed = role?.trim(); + final fileIdTrimmed = fileId?.trim(); + final endpoint = Uri.parse('${Env.apiUrl}grid'); + // final endpoint = Uri.parse('http://localhost/nhance_partner_be/grid'); + final queryParameters = {}; + if (roleTrimmed != null && roleTrimmed.isNotEmpty) { + queryParameters['role'] = roleTrimmed; + } + if (fileIdTrimmed != null && fileIdTrimmed.isNotEmpty) { + queryParameters['file_id'] = fileIdTrimmed; + } + final url = endpoint.replace(queryParameters: queryParameters); + + final headers = { + 'Authorization': 'Bearer ${_token ?? ''}', + 'app-signature': Env.App_Signature, + }; + + return await _makeGetRequest(url, headers); + } + + Future downloadGridExcel({ + String? role, + String? fileId, + String? insurer, + String? vehicleType, + String? segment, + String? rto, + String? planType, + String? search, + String? loggedId, + }) async { + if (_token == null) { + await _initializeToken(); + } + + final endpoint = Uri.parse('${Env.apiUrl}grid/download'); + // final endpoint = Uri.parse('http://localhost/nhance_partner_be/grid/download'); + final queryParameters = {}; + + void addQuery(String key, String? value) { + final v = value?.trim(); + if (v != null && v.isNotEmpty) { + queryParameters[key] = v; + } + } + + addQuery('role', role); + addQuery('file_id', fileId); + addQuery('insurer', insurer); + addQuery('vehicle_type', vehicleType); + addQuery('segment', segment); + addQuery('rto', rto); + addQuery('plan_type', planType); + addQuery('search', search); + addQuery('logged_id', loggedId); + + final url = endpoint.replace(queryParameters: queryParameters); + final headers = { + 'Authorization': 'Bearer ${_token ?? ''}', + 'app-signature': Env.App_Signature, + }; + + final response = await _makeGethttpRequest(url, headers); + if (response.statusCode != 200) { + throw Exception('Failed to download grid excel'); + } + + final contentType = response.headers['content-type'] ?? ''; + if (contentType.contains('application/json')) { + throw Exception('No export data available for selected filters'); + } + + final blob = html.Blob([response.bodyBytes]); + final blobUrl = html.Url.createObjectUrlFromBlob(blob); + final fileName = extractFileName( + response.headers['content-disposition'], + 'grid_export.xlsx', + ); + html.AnchorElement(href: blobUrl) + ..setAttribute('download', fileName) + ..click(); + html.Url.revokeObjectUrl(blobUrl); + } + Future> fetchManagerIncentiveList(mangerId) async { // print(_token); if (_token == null) { @@ -1246,6 +1353,70 @@ class ApiService { return response; } + // ───────────────────────────────────────────────────────────────────────────── + /// GET /partner/{id}/details + /// Returns: profile info, policy count, premium totals, commission, clients + Future> getPartnerDetails(dynamic id) async { + if (_token == null) await _initializeToken(); + + final url = Uri.parse('${Env.apiUrl}partner/$id/details'); + final headers = { + 'Authorization': 'Bearer $_token', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + /// GET /partner/{id}/policies + /// Returns: list of policies with policy_no, holder_name, product, premium, status + Future> getPartnerPolicies(dynamic id) async { + if (_token == null) await _initializeToken(); + + final url = Uri.parse('${Env.apiUrl}partner/$id/policies'); + + final headers = { + 'Authorization': 'Bearer $_token', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + /// GET /partner/{id}/renewals?days=N + /// Returns: list of policies expiring within [days] days + /// [days] default is 20 — pass 10, 20, 30 or 45 from the UI dropdown + Future> getPartnerRenewals( + dynamic id, { + int days = 20, + }) async { + if (_token == null) await _initializeToken(); + + final url = Uri.parse('${Env.apiUrl}partner/$id/renewals?days=$days'); + final headers = { + 'Authorization': 'Bearer $_token', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + /// GET /partner/{id}/earnings + /// Returns: list of monthly earning records with + /// month_key, month_label, premium, policies, paid/status + Future> getPartnerEarnings(dynamic id) async { + if (_token == null) await _initializeToken(); + + final url = Uri.parse('${Env.apiUrl}partner/$id/earnings'); + final headers = { + 'Authorization': 'Bearer $_token', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + // ───────────────────────────────────────────────────────────────────────────── Future> findEnqQuotePolicyView(id) async { print('findEnqQuotePolicyVie2w - $id'); if (_token == null) { @@ -1415,6 +1586,8 @@ class ApiService { String? selectedStaffId, String ? selectedStatus, String ? selectedInsurer, + /// Accounts + policy list: filter by partner (agent id). + String? selectedAgentId, }) async { print( "fetchPolicyDataOnlyListAPI - mangerID- $managerId - id - $id - role -$role >> selectedInsurer -$selectedInsurer ", @@ -1441,12 +1614,15 @@ class ApiService { query = 'agent_id=$id'; } - final url = Uri.parse( - '${Env.apiUrl}enquiry/enquiryList?$query&only_policy_data=true&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&staff_id=$selectedStaffId', - ); - // final url = Uri.parse( - // 'http://localhost/nhance_partner_be/enquiry/enquiryList?$query&only_policy_data=true&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&staff_id=$selectedStaffId', - // ); + var listPath = + '${Env.apiUrl}enquiry/enquiryList?$query&only_policy_data=true&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&staff_id=${selectedStaffId ?? ''}'; + if (role == 'Accounts' && + selectedAgentId != null && + selectedAgentId.toString().trim().isNotEmpty) { + listPath += + '&agent_id=${Uri.encodeComponent(selectedAgentId.toString().trim())}'; + } + final url = Uri.parse(listPath); // if (role == 'manager') { // url = Uri.parse( @@ -1565,16 +1741,16 @@ class ApiService { // ----------------------------------- ENDORSEMENT ------------------------------------------------- Future> fetchEndorsementList( - managerId, - int id, - role, { - String? fromDate, - String? toDate, - String? endorsementType, - String? insurerId, - String? status, - String? verification, - }) async { + managerId, + int id, + role, { + String? fromDate, + String? toDate, + String? endorsementType, + String? insurerId, + String? status, + String? verification, + }) async { print('fetchEndorsementList in'); if (_token == null) { await _initializeToken(); @@ -1604,7 +1780,7 @@ class ApiService { print('fetchEndorsementList api'); final url = Uri.parse('${Env.apiUrl}endorsement/endorsementList') - .replace(queryParameters: queryParams); + .replace(queryParameters: queryParams); final headers = { 'Authorization': 'Bearer $_token', @@ -1760,7 +1936,7 @@ class ApiService { return response; } - Future> fetchAgentUnusedCommissionList(id,broker_id) async { + Future> fetchAgentUnusedCommissionList(id) async { print('fetchAGENTNameDropDown'); if (_token == null) { await _initializeToken(); @@ -1768,9 +1944,13 @@ class ApiService { dynamic url; print('fetchAGENTNameDropDown 1'); - url = Uri.parse( - '${Env.apiUrl}invoice/getAgentUnusedCommissionList?manager_id=$id&broker_id=$broker_id', - ); + // url = Uri.parse( + // '${Env.apiUrl}invoice/getAgentUnusedCommissionList?manager_id=$id', + // ); + + // url = Uri.parse( + // 'http://localhost/nhance_partner_be/invoice/getAgentUnusedCommissionList?manager_id=$id', + // ); print('fetchAGENTNameDropDown 2'); final headers = { 'Authorization': 'Bearer $_token' ?? '', @@ -1870,7 +2050,8 @@ class ApiService { // --------------------------------- PayOut Module---------------------------------------------- Future> getCommissionRateList(data) async { print("getCommissionRateList------- $data}"); - final url = Uri.parse('${Env.apiUrl}/invoice/commission-rate-list'); + // final url = Uri.parse('${Env.apiUrl}/invoice/commission-rate-list'); + // final url = Uri.parse('http://localhost/nhance_partner_be/invoice/commission-rate-list'); print("getCommissionRateList 1"); // final token = await getToken(); // Fetch token @@ -1896,7 +2077,8 @@ class ApiService { } Future> getCreateOrUpdate(data) async { - final url = Uri.parse('${Env.apiUrl}invoice/create-or-update'); + final url = Uri.parse('${Env.apiUrl}invoice/create-or-update'); + // final url = Uri.parse('http://localhost/nhance_partner_be/invoice/create-or-update'); // final token = await getToken(); // Fetch token @@ -1933,6 +2115,23 @@ class ApiService { return response; } + Future> getPayoutList() async { + // print(_token); + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}invoice/list'); + //final url = Uri.parse('http://localhost/nhance_partner_be/invoice/list'); + + final headers = { + 'Authorization': 'Bearer $_token' ?? '', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + Future> deleteInvoice(ID) async { // print(_token); if (_token == null) { @@ -1981,6 +2180,231 @@ class ApiService { return response; } + Future> addInvoicePayment( + Map data, + ) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}invoice/add-payment'); + // final url = Uri.parse('http://localhost/nhance_partner_be/invoice/add-payment'); + + final headers = { + 'Authorization': 'Bearer $_token', + 'Content-Type': 'application/json', + 'app-signature': Env.App_Signature, + }; + + final response = await _makePostRequestJson(url, data, headers); + return response; + } + + Future> getInvoiceUtrDetails(dynamic invoiceId) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}invoice/utrDetails?invoice_id=$invoiceId'); + //final url = Uri.parse('http://localhost/nhance_partner_be/invoice/utrDetails?invoice_id=$invoiceId'); + + final headers = { + 'Authorization': 'Bearer $_token' ?? '', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + Future> updateInvoiceUtrDetails( + Map data, + ) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}invoice/updateUtrDetails'); + // final url = Uri.parse('http://localhost/nhance_partner_be/invoice/updateUtrDetails'); + final headers = { + 'Authorization': 'Bearer $_token', + 'Content-Type': 'application/json', + 'app-signature': Env.App_Signature, + }; + final response = await _makePostRequestJson(url, data, headers); + return response; + } + + Future> addInvoicePaymentHistory(dynamic invoiceId) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse( + '${Env.apiUrl}invoice/add-payment-history?invoice_id=$invoiceId', + ); + // final url = Uri.parse( + // 'http://localhost/nhance_partner_be/invoice/add-payment-history?invoice_id=$invoiceId', + // ); + + final headers = { + 'Authorization': 'Bearer $_token' ?? '', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + Future> listInvoiceUtrDetails(dynamic invoiceId) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse( + '${Env.apiUrl}invoice/listUtrDetails?invoice_id=$invoiceId', + ); + // final url = Uri.parse( + // 'http://localhost/nhance_partner_be/invoice/listUtrDetails?invoice_id=$invoiceId', + // ); + final headers = { + 'Authorization': 'Bearer $_token' ?? '', + 'app-signature': Env.App_Signature, + }; + final response = await _makeGetRequest(url, headers); + return response; + } + + Future> bulkUploadInvoiceCommission({ + required PlatformFile file, + required Map data, + }) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}invoice/bulk-upload-commission'); + try { + final request = http.MultipartRequest('POST', url); + request.headers.addAll({ + 'Authorization': 'Bearer $_token', + 'app-signature': Env.App_Signature, + }); + + if (file.bytes == null) throw Exception('Could not read file bytes'); + + request.files.add( + http.MultipartFile.fromBytes( + 'file_name', + file.bytes!, + filename: file.name, + ), + ); + + data.forEach((key, value) { + if (value != null) { + request.fields[key] = value.toString(); + } + }); + + final streamedResponse = await request.send(); + final responseBody = await streamedResponse.stream.bytesToString(); + + if (streamedResponse.statusCode == 401 || + streamedResponse.statusCode == 403) { + await clearLocalStorageAndRedirect(); + return {'status': 'error', 'message': 'Session expired'}; + } + + if (streamedResponse.statusCode != 200) { + return { + 'status': 'error', + 'message': 'Server Error: ${streamedResponse.statusCode}', + }; + } + + final decoded = jsonDecode(responseBody); + return decoded is Map + ? decoded + : {'status': 'error', 'message': 'Unexpected response format'}; + } catch (e) { + return {'status': 'error', 'message': e.toString()}; + } + } + + Future> bulkUploadInvoiceCommissionProceed( + Map data, + ) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}invoice/bulk-upload-commission/proceed'); + + final headers = { + 'Authorization': 'Bearer $_token', + 'Content-Type': 'application/json', + 'app-signature': Env.App_Signature, + }; + + final response = await _makePostRequestJson(url, data, headers); + return response; + } + + Future> uploadPolicyCommissionExcel({ + required dynamic id, + required PlatformFile file, + bool proceedPartnerMismatch = false, + }) async { + if (_token == null) { + await _initializeToken(); + } + + final url = Uri.parse('${Env.apiUrl}policy/uploadCommissionExcel'); + try { + final request = http.MultipartRequest('POST', url); + request.headers.addAll({ + 'Authorization': 'Bearer $_token', + 'app-signature': Env.App_Signature, + }); + + if (file.bytes == null) throw Exception('Could not read file bytes'); + + request.files.add( + http.MultipartFile.fromBytes( + 'file', + file.bytes!, + filename: file.name, + ), + ); + request.fields['id'] = id.toString(); + if (proceedPartnerMismatch) { + request.fields['proceed_partner_mismatch'] = '1'; + } + + final streamedResponse = await request.send(); + final responseBody = await streamedResponse.stream.bytesToString(); + + if (streamedResponse.statusCode == 401 || + streamedResponse.statusCode == 403) { + await clearLocalStorageAndRedirect(); + return {'status': 'error', 'message': 'Session expired'}; + } + + if (streamedResponse.statusCode != 200) { + return { + 'status': 'error', + 'message': 'Server Error: ${streamedResponse.statusCode}', + }; + } + + final decoded = jsonDecode(responseBody); + return decoded is Map + ? decoded + : {'status': 'error', 'message': 'Unexpected response format'}; + } catch (e) { + return {'status': 'error', 'message': e.toString()}; + } + } + Future> findPolicyApi(ID) async { // print(_token); if (_token == null) { @@ -2059,6 +2483,24 @@ class ApiService { return response; } + Future> updatePolicyCommissionApi(data) async { + final url = Uri.parse('${Env.apiUrl}policy/updatePolicyCommission'); + //final url = Uri.parse('http://localhost/nhance_partner_be/policy/updatePolicyCommission'); + + if (_token == null) { + throw Exception('Token not found. Please log in.'); + } + + final headers = { + 'Authorization': 'Bearer $_token', + 'Content-Type': 'application/json', + 'app-signature': Env.App_Signature, + }; + + final response = await _makePostRequestJson(url, data, headers); + return response; + } + Future> fetchPolicyMasterDropDown(String val) async { // print(_token); if (_token == null) { @@ -2088,7 +2530,6 @@ class ApiService { final url = Uri.parse( '${Env.apiUrl}reports/endorsement-excel?manager_id=$managerId', ); - // final url = Uri.parse('http://localhost/nhance_partner_be/reports/endorsement-excel?manager_id=$managerId'); print('getPdfDownload endorsement-excel - $url'); await _initializeToken(); @@ -2153,6 +2594,25 @@ class ApiService { } } + /// Payout sample: `from_date` / `to_date` as `yyyy-MM-dd`, `agent_code` as partner code (may be empty). + Future downloadPendingCommissionExcel({ + required String fromDate, + required String toDate, + required String agentCode, + }) async { + final query = Uri( + queryParameters: { + 'from_date': fromDate, + 'to_date': toDate, + 'agent_code': agentCode, + }, + ).query; + await getPdfDownload( + 'policy/downloadPendingCommissionExcel?$query', + 'pending_commission', + ); + } + Future generatePolicyExcel(managerId, fromDate, toDate, searchValue,flag,insurer) async { final url = Uri.parse( '${Env.apiUrl}reports/policy-excel?manager_id=$managerId&from_date=${fromDate ?? ''}&to_date=${toDate ?? ''}&search=${searchValue ?? ''}&show_policy_report=${flag ?? ''}&insurer_id=${insurer ?? ''}', @@ -2275,7 +2735,6 @@ class ApiService { await _initializeToken(); } final url = Uri.parse('${Env.apiUrl}endorsement/deleteEndorsement?id=$id'); - // final url = Uri.parse('http://localhost/nhance_partner_be/endorsement/deleteEndorsement?id=$id'); final headers = { 'Authorization': 'Bearer $_token' ?? '', @@ -2295,7 +2754,6 @@ class ApiService { } final url = Uri.parse('${Env.apiUrl}endorsement/uploadEndorsementFile'); - // final url = Uri.parse('http://localhost/nhance_partner_be/endorsement/uploadEndorsementFile'); try { final request = http.MultipartRequest('POST', url); diff --git a/lib/presentation/layouts/appheader.dart b/lib/presentation/layouts/appheader.dart index dbe7815..485c14d 100644 --- a/lib/presentation/layouts/appheader.dart +++ b/lib/presentation/layouts/appheader.dart @@ -63,34 +63,55 @@ class _AppHeaderState extends ConsumerState { // Set active menu based on current route void _setActiveMenu() { WidgetsBinding.instance.addPostFrameCallback((_) { - final currentRoute = GoRouterState.of(context).uri.path; + final currentRoute = GoRouterState.of(context).uri.path.toLowerCase(); setState(() { - if (currentRoute.contains('dashboard')) { - _activeMenu = 'Dashboard'; - } else if (currentRoute.contains('enquiry')) { - _activeMenu = 'Enquiry'; - } else if (currentRoute.contains('agent') || - currentRoute.contains('staff') || - currentRoute.contains('salesExecutive')) { - _activeMenu = 'User'; - } else if (currentRoute.contains('claim') || - currentRoute.contains('endosement') || - currentRoute.contains('policy') || - currentRoute.contains('attendance')) { - _activeMenu = 'Reports'; - } else if (currentRoute.contains('broker') || - currentRoute.contains('payment')) { - _activeMenu = 'Masters'; - } else if (currentRoute.contains('invoice') || // ✅ ADD THIS - currentRoute.contains('payout')) { - _activeMenu = 'Invoice'; - } else if (currentRoute.contains('Endorsement')) { - _activeMenu = 'Endorsement'; - } + _activeMenu = _menuKeyForRoute(currentRoute); }); }); } + String? _menuKeyForRoute(String currentRoute) { + final route = currentRoute.toLowerCase(); + if (route.contains('dashboard') || + route.contains('partnerportaldashboard')) { + return 'Dashboard'; + } + if (route.contains('enquiry')) { + return 'Enquiry'; + } + if (route.contains('endorsement')) { + return 'Endorsement'; + } + if (route.contains('agent') || + route.contains('staff') || + route.contains('salesexecutive') || + route.contains('pos')) { + return 'User'; + } + if (route.contains('claim') || + route.contains('policy') || + route.contains('attendance') || + route.contains('monthlycommission') || + route.contains('gridlist') || + route.contains('gridview')) { + return 'Reports'; + } + if (route.contains('broker') || + route.contains('payment') || + route.contains('vehicletype') || + route.contains('endorsementtype')) { + return 'Masters'; + } + if (route.contains('payoutgrid')) { + return 'PayoutGrid'; + } + if (route.contains('invoice') || + (route.contains('payout') && !route.contains('payoutgrid'))) { + return 'Payout'; + } + return null; + } + Future _initializeToken() async { _token = await AuthService.getToken(); if (_token != null) { @@ -116,7 +137,7 @@ class _AppHeaderState extends ConsumerState { _queueTimer = Timer.periodic( const Duration(seconds: 30), - (_) => getStaffLevelCount( + (_) => getStaffLevelCount( profileData?['manager_id'], profileData?['id'], ), @@ -249,6 +270,23 @@ class _AppHeaderState extends ConsumerState { ), ], if (key == 'Reports') ...[ + if (role == 'manager' || + role == 'Accounts' || + role == 'agent') ...[ + _buildPopupItem( + label: "Grid List", + onTap: () { + _hidePopup(); + setState(() => _activeMenu = 'Reports'); + context.go( + role == 'agent' + ? AppRoutes.gridView + : AppRoutes.gridList, + ); + }, + ), + const SizedBox(height: 2), + ], if (role == 'manager') ...[ _buildPopupItem( label: "Attendance", @@ -342,6 +380,13 @@ class _AppHeaderState extends ConsumerState { @override Widget build(BuildContext context) { final roleId = ref.watch(userRoleProvider); + final currentRouteLower = + GoRouterState.of(context).uri.path.toLowerCase(); + var activeMenu = + _menuKeyForRoute(currentRouteLower) ?? _activeMenu; + if (currentRouteLower.contains('payoutgrid') && roleId != 'agent') { + activeMenu = 'Reports'; + } return AppBar( // backgroundColor: const Color(0xFFD6F6F4), @@ -366,12 +411,15 @@ class _AppHeaderState extends ConsumerState { _buildMenuItem( icon: Icons.dashboard, label: "Dashboard", - isActive: _activeMenu == 'Dashboard', + isActive: activeMenu == 'Dashboard', onTap: () async { _hidePopup(); setState(() => _activeMenu = 'Dashboard'); await _clearDashboardFilters(); - context.go(AppRoutes.dashboard); + final dashboardRoute = roleId == 'agent' + ? AppRoutes.partnerPortalDashboard + : AppRoutes.dashboard; + context.go(dashboardRoute); }, ), @@ -386,7 +434,7 @@ class _AppHeaderState extends ConsumerState { _buildMenuItem( icon: Icons.list_alt_rounded, label: "Enquiry", - isActive: _activeMenu == 'Enquiry', + isActive: activeMenu == 'Enquiry', onTap: () async { _hidePopup(); setState(() => _activeMenu = 'Enquiry'); @@ -402,7 +450,7 @@ class _AppHeaderState extends ConsumerState { const SizedBox(width: 8), _buildMenuItem( - isActive: _activeMenu == 'Endorsement', + isActive: activeMenu == 'Endorsement', icon: Icons.checklist_outlined, label: "Endorsement", onTap: () async { @@ -413,8 +461,6 @@ class _AppHeaderState extends ConsumerState { }, ), - - const SizedBox(width: 8), // User (Manager Only) - with hover popup @@ -423,7 +469,7 @@ class _AppHeaderState extends ConsumerState { icon: Icons.person_add_alt, label: "User", popupKey: 'User', - isActive: _activeMenu == 'User', + isActive: activeMenu == 'User', ), const SizedBox(width: 8), ], @@ -434,7 +480,7 @@ class _AppHeaderState extends ConsumerState { icon: Icons.settings_suggest_outlined, label: "Masters", popupKey: 'Masters', - isActive: _activeMenu == 'Masters', + isActive: activeMenu == 'Masters', ), const SizedBox(width: 8), ], @@ -445,27 +491,26 @@ class _AppHeaderState extends ConsumerState { icon: Icons.receipt_long, label: "Reports", popupKey: 'Reports', - isActive: _activeMenu == 'Reports', + isActive: activeMenu == 'Reports', ), const SizedBox(width: 8), ], - // Pay Out button for Accounts + + // Payout (Accounts only) — single top-level item, no submenu if (role == 'Accounts') ...[ _buildMenuItem( - isActive: _activeMenu == 'Invoice', - icon: Icons.checklist_outlined, - label: "Invoice", - onTap: () async { + icon: Icons.payments_outlined, + label: "Payout", + isActive: activeMenu == 'Payout', + onTap: () { _hidePopup(); - setState(() => _activeMenu = 'Invoice'); - await _clearDashboardFilters(); - context.go(AppRoutes.invoiceList); + setState(() => _activeMenu = 'Payout'); + context.go(AppRoutes.payoutList); }, ), ], - const Spacer(), // Raise Enquiry button for agents @@ -666,11 +711,7 @@ class _AppHeaderState extends ConsumerState { return MouseRegion( onEnter: (_) { final box = iconContext.findRenderObject() as RenderBox; - _showQueuePopup( - context, - box.localToGlobal(Offset.zero), - box.size, - ); + _showQueuePopup(context, box.localToGlobal(Offset.zero), box.size); }, onExit: (_) => _hidePopup(), child: Stack( @@ -689,28 +730,30 @@ class _AppHeaderState extends ConsumerState { // 🔴 Badge // if (queueCount > 0) - Positioned( - right: -6, - top: -6, - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - constraints: - const BoxConstraints(minWidth: 18, minHeight: 18), - child: Text( - queueCount.toString(), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), + Positioned( + right: -6, + top: -6, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 18, + minHeight: 18, + ), + child: Text( + queueCount.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, ), ), ), + ), ], ), ); @@ -765,17 +808,17 @@ class _AppHeaderState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, - style: - GoogleFonts.inter(fontSize: 12, color: Colors.grey.shade700)), + style: + GoogleFonts.inter(fontSize: 12, color: Colors.grey.shade700)), Text(value.toString(), - style: GoogleFonts.inter( - fontSize: 12, fontWeight: FontWeight.w600)), + style: GoogleFonts.inter( + fontSize: 12, fontWeight: FontWeight.w600)), ], ), ); } - + // Simple menu item widget Widget _buildMenuItem({ diff --git a/lib/presentation/screens/Enquiry/enquiry/policy_tab.dart b/lib/presentation/screens/Enquiry/enquiry/policy_tab.dart index a569d2b..ec1bd57 100644 --- a/lib/presentation/screens/Enquiry/enquiry/policy_tab.dart +++ b/lib/presentation/screens/Enquiry/enquiry/policy_tab.dart @@ -157,7 +157,7 @@ class PolicyTabState extends ConsumerState { controllers['policyNo']?.text = record['policy_number']?.toString() ?? ''; controllers['paymentMode']?.text = - record['payment_mode']?.toString() ?? ''; + record['payment_mode_value']?.toString() ?? ''; controllers['premAmount']?.text = record['premium_amount']?.toString() ?? ''; controllers['planType']?.text = diff --git a/lib/presentation/screens/Enquiry/enquiry/updateEndorsementDialog.dart b/lib/presentation/screens/Enquiry/enquiry/updateEndorsementDialog.dart index 715b2b6..af1d3d4 100644 --- a/lib/presentation/screens/Enquiry/enquiry/updateEndorsementDialog.dart +++ b/lib/presentation/screens/Enquiry/enquiry/updateEndorsementDialog.dart @@ -19,6 +19,10 @@ import '../../../themes/indicators/search_field_theme.dart'; import '../../../themes/indicators/text_field_theme.dart'; import '../../../providers/manager_provider.dart'; +final decimalFormatter = [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), +]; + class UpdateEndorsementDialog extends StatefulWidget { final Map item; @@ -179,6 +183,7 @@ class _UpdateEndorsementDialogState extends State { try { final Uri uri = Uri.parse('${Env.apiUrl}endorsement/updateEndorsement'); + // final Uri uri = Uri.parse('http://localhost/nhance_partner_be/endorsement/updateEndorsement'); if (_token == null) throw Exception('Token not found. Please log in.'); @@ -739,9 +744,12 @@ class _UpdateEndorsementDialogState extends State { backgroundColor: Color(0xFFEDF6F5), controller: controllers['endorsement_premium']!, validator: (value) => Validators.requiredField(value, "endorsement_premium"), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), - ], + keyboardType: const TextInputType.numberWithOptions(decimal: true), + /* + * Allow decimal amount (e.g. 6000.00). + * Integer-only formatter was removing "." and corrupting value. + */ + inputFormatters: decimalFormatter, txtwidth: ResponsiveLayout.isMobile(context) ? null : MediaQuery.of(context).size.width * 0.26, diff --git a/lib/presentation/screens/Enquiry/enquiryList.dart b/lib/presentation/screens/Enquiry/enquiryList.dart index c95add9..1f0adac 100644 --- a/lib/presentation/screens/Enquiry/enquiryList.dart +++ b/lib/presentation/screens/Enquiry/enquiryList.dart @@ -199,8 +199,16 @@ class EnquiryListState extends ConsumerState { if (response['status'] == 'success') { final data = response['data']; - final fromDate = response['from_date'] ?? ''; - final toDate = response['to_date'] ?? ''; + /* + * Some APIs do not return from_date/to_date in the payload. + * Keep UI date fields stable by falling back to the request dates. + */ + final fromDate = (response['from_date'] ?? '').toString().trim().isNotEmpty + ? response['from_date'].toString() + : (fromDt?.toString() ?? ''); + final toDate = (response['to_date'] ?? '').toString().trim().isNotEmpty + ? response['to_date'].toString() + : (toDt?.toString() ?? ''); print('FromDate : $fromDate'); print('ToDate : $toDate'); diff --git a/lib/presentation/screens/Enquiry/policy_claims_endros/create_endorsement_dialog.dart b/lib/presentation/screens/Enquiry/policy_claims_endros/create_endorsement_dialog.dart index b1d4e06..1d4a663 100644 --- a/lib/presentation/screens/Enquiry/policy_claims_endros/create_endorsement_dialog.dart +++ b/lib/presentation/screens/Enquiry/policy_claims_endros/create_endorsement_dialog.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; import '../../../../core/config/env.dart'; import '../../../../core/services/api_service.dart'; import '../../../../data/services/auth_service.dart'; @@ -1709,26 +1710,40 @@ class _CreateEndorsementDialogState policyStartController, isStartDateFocused, (val) => setState(() => isStartDateFocused = val), + firstDate: DateTime(2000), + lastDate: DateTime(2100), ); } Widget buildPolicyEndDate(context) { + // Parse start date as minDate, fallback to DateTime(2000) + DateTime minDate = DateTime(2000); + if (policyStartController.text.isNotEmpty) { + try { + minDate = DateFormat('dd-MM-yyyy').parse(policyStartController.text); + } catch (_) {} + } + return buildDateField( context, "Policy End Date", policyEndController, isEndDateFocused, - (val) => setState(() => isEndDateFocused = val), + (val) => setState(() => isEndDateFocused = val), + firstDate: minDate, // 👈 min = policy start date + lastDate: DateTime(2100), // 👈 allow future ); } Widget buildDateField( - BuildContext context, - String label, - TextEditingController controller, - bool isFocused, - Function(bool) onFocusChanged, - ) { + BuildContext context, + String label, + TextEditingController controller, + bool isFocused, + Function(bool) onFocusChanged, { + DateTime? firstDate, // 👈 ADD + DateTime? lastDate, // 👈 ADD + }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1764,9 +1779,31 @@ class _CreateEndorsementDialogState DateTime? picked = await showDatePicker( context: context, - firstDate: DateTime(2000), - lastDate: DateTime.now(), - initialDate: DateTime.now(), + firstDate: firstDate ?? DateTime(2000), + lastDate: lastDate ?? DateTime(2100), + + // ✅ Clamp initialDate so it's always inside firstDate..lastDate + initialDate: () { + final fd = firstDate ?? DateTime(2000); + final ld = lastDate ?? DateTime(2100); + + DateTime initial = DateTime.now(); + + // Try to parse existing controller value + if (controller.text.isNotEmpty) { + try { + initial = DateFormat('dd-MM-yyyy').parse(controller.text); + } catch (_) {} + } + + // Clamp: if initial is before firstDate, use firstDate + if (initial.isBefore(fd)) return fd; + + // Clamp: if initial is after lastDate, use lastDate + if (initial.isAfter(ld)) return ld; + + return initial; + }(), ); onFocusChanged(false); diff --git a/lib/presentation/screens/Enquiry/policy_claims_endros/endorsomentUpdateValidation.dart b/lib/presentation/screens/Enquiry/policy_claims_endros/endorsomentUpdateValidation.dart index 7c9c4a8..f6cfe9e 100644 --- a/lib/presentation/screens/Enquiry/policy_claims_endros/endorsomentUpdateValidation.dart +++ b/lib/presentation/screens/Enquiry/policy_claims_endros/endorsomentUpdateValidation.dart @@ -83,6 +83,7 @@ class _endorsomentValidationState extends ConsumerState { String? selectedFileNames; String? selectedPaymentModeId; + String? selectedPaymentModeValue; bool isPolicyFromFocused = false; bool isEndorsementTypeFocused = false; @@ -322,6 +323,7 @@ class _endorsomentValidationState extends ConsumerState { void setEditValues() { final item = widget.item; + print("IITTTEM - $item"); if (item == null) return; final policyFromValue = item['policy_from']?.toString() ?? ''; @@ -335,7 +337,10 @@ class _endorsomentValidationState extends ConsumerState { // pendingDaysController.text = item['pending_days']?.toString() ?? ''; premiumController.text = item['endorsement_premium']?.toString() ?? ''; commissionController.text = item['commission_amount']?.toString() ?? ''; - selectedPaymentModeId = item['payment_mode_id']?.toString(); + selectedPaymentModeId = + item['payment_mode_id']?.toString() ?? item['payment_mode']?.toString(); + selectedPaymentModeValue = + item['payment_mode_value']?.toString() ?? item['value']?.toString(); insuredNameController.text = item['insured_name'] ?? ''; regNoController.text = item['reg_no'] ?? ''; @@ -394,12 +399,6 @@ class _endorsomentValidationState extends ConsumerState { setState(() { getPaymentModeData = List>.from(response['data']); filteredPaymentModeData = List.from(getPaymentModeData); - - // ✅ Re-apply selectedPaymentModeId AFTER list is loaded - final savedId = widget.item?['payment_mode_id']?.toString(); - if (savedId != null) { - selectedPaymentModeId = savedId; - } }); } } catch (e) { @@ -677,6 +676,7 @@ class _endorsomentValidationState extends ConsumerState { }); final Uri uri = Uri.parse('${Env.apiUrl}endorsement/updateEndorsement'); + // final Uri uri = Uri.parse('http://localhost/nhance_partner_be/endorsement/updateEndorsement'); if (_token == null) { throw Exception('Token not found. Please log in.'); @@ -749,13 +749,32 @@ class _endorsomentValidationState extends ConsumerState { print("Response status: ${response.statusCode}"); print("Response body: ${response.body}"); + /* + * Treat API success only when payload status is "success". + * Some endpoints can return 200 with a failure body, which causes false UI success. + */ if (response.statusCode == 200 || response.statusCode == 201) { print("✅ Partner submitted successfully!"); - print("Response: ${response.body}"); - ToastHelper.showSuccessToast(context, 'Saved Successfully'); - context.pop(true); + final dynamic responseBody = jsonDecode(response.body); + final bool isApiSuccess = + (responseBody?['status']?.toString().toLowerCase() == 'success'); + + if (!isApiSuccess) { + final String backendMessage = responseBody?['data']?.toString() ?? + responseBody?['message']?.toString() ?? + 'Failed To Save'; + ToastHelper.showErrorToast(context, backendMessage); + return; + } + + final bool isClosedStatus = _isClosedStatusFromResponse(response.body); + final String successMessage = isClosedStatus + ? 'Data accuracy confirmed. Status changed to Closed.' + : 'Saved Successfully'; + ToastHelper.showSuccessToast(context, successMessage); + context.pop(true); widget.onSubmit("success"); } else if (response.statusCode == 403) { await apiService.clearLocalStorageAndRedirect(); @@ -779,6 +798,41 @@ class _endorsomentValidationState extends ConsumerState { } } + bool _isClosedStatusFromResponse(String responseBody) { + try { + final dynamic decoded = jsonDecode(responseBody); + final String statusText = + (decoded?['data']?['status']?.toString().toLowerCase() ?? ''); + final String messageText = + (decoded?['message']?.toString().toLowerCase() ?? ''); + return statusText == 'closed' || messageText.contains('closed'); + } catch (_) { + return responseBody.toLowerCase().contains('closed'); + } + } + + Future _confirmDataAccuracyChange(BuildContext context) async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('Confirm Data Accuracy'), + content: const Text('The status will be automatically updated to "Closed."'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Confirm'), + ), + ], + ), + ) ?? + false; + } + String get pdfDocumentTitle { if (existingEndorsementFileName != null && existingEndorsementFileName!.isNotEmpty) { return 'Endorsement Document'; @@ -1580,7 +1634,7 @@ class _endorsomentValidationState extends ConsumerState { child: ElevatedButton( onPressed: (!isVerifiedAllowed || isSubmitLoading) ? null - : () { + : () async { if (!_formKeyEndrosment.currentState!.validate()) return; if (showFinancialFields && !hasCommission) { @@ -1591,8 +1645,18 @@ class _endorsomentValidationState extends ConsumerState { return; } + final bool isAccountsRole = roleId == 'Accounts'; + if (isAccountsRole) { + final bool confirmed = + await _confirmDataAccuracyChange(context); + if (!confirmed) return; + } + final dataSet = endrosmentDetails(); - createUserData(dataSet); + if (isAccountsRole) { + dataSet['status'] = 'Closed'; + } + await createUserData(dataSet); }, style: ElevatedButton.styleFrom( @@ -2296,16 +2360,28 @@ class _endorsomentValidationState extends ConsumerState { policyStartController, isStartDateFocused, (val) => setState(() => isStartDateFocused = val), + firstDate: DateTime(2000), + lastDate: DateTime(2100), ); } Widget buildPolicyEndDate(context) { + // Parse start date as minDate, fallback to DateTime(2000) + DateTime minDate = DateTime(2000); + if (policyStartController.text.isNotEmpty) { + try { + minDate = DateFormat('dd-MM-yyyy').parse(policyStartController.text); + } catch (_) {} + } + return buildDateField( context, "Policy End Date", policyEndController, isEndDateFocused, (val) => setState(() => isEndDateFocused = val), + firstDate: minDate, // 👈 min = policy start date + lastDate: DateTime(2100), ); } @@ -2315,6 +2391,10 @@ class _endorsomentValidationState extends ConsumerState { TextEditingController controller, bool isFocused, Function(bool) onFocusChanged, + { + DateTime? firstDate, // 👈 ADD + DateTime? lastDate, // 👈 ADD + } ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -2346,12 +2426,33 @@ class _endorsomentValidationState extends ConsumerState { child: GestureDetector( onTap: () async { onFocusChanged(true); - DateTime? picked = await showDatePicker( context: context, - firstDate: DateTime(2000), - lastDate: DateTime.now(), - initialDate: DateTime.now(), + firstDate: firstDate ?? DateTime(2000), + lastDate: lastDate ?? DateTime(2100), + + // ✅ Clamp initialDate so it's always inside firstDate..lastDate + initialDate: () { + final fd = firstDate ?? DateTime(2000); + final ld = lastDate ?? DateTime(2100); + + DateTime initial = DateTime.now(); + + // Try to parse existing controller value + if (controller.text.isNotEmpty) { + try { + initial = DateFormat('dd-MM-yyyy').parse(controller.text); + } catch (_) {} + } + + // Clamp: if initial is before firstDate, use firstDate + if (initial.isBefore(fd)) return fd; + + // Clamp: if initial is after lastDate, use lastDate + if (initial.isAfter(ld)) return ld; + + return initial; + }(), ); onFocusChanged(false); @@ -2498,8 +2599,12 @@ class _endorsomentValidationState extends ConsumerState { label: "Endorsement Premium", controller: premiumController, isRequired: showFinancialFields, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: const TextInputType.numberWithOptions(decimal: true), + /* + * Keep decimal values (e.g. 6000.00). + * digitsOnly strips "." and turns 6000.00 into 600000. + */ + inputFormatters: decimalFormatter, isFocused: isPremiumFocused, onFocusChanged: (val) { setState(() => isPremiumFocused = val); @@ -2512,8 +2617,12 @@ class _endorsomentValidationState extends ConsumerState { label: "Commission Amount", controller: commissionController, isRequired: showFinancialFields, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: const TextInputType.numberWithOptions(decimal: true), + /* + * Keep decimal values (e.g. 2900.50). + * digitsOnly strips "." and corrupts monetary amounts. + */ + inputFormatters: decimalFormatter, isFocused: isCommissionFocused, onFocusChanged: (val) { setState(() => isCommissionFocused = val); @@ -2716,7 +2825,7 @@ class _endorsomentValidationState extends ConsumerState { // 🔥 Clear old values premiumController.clear(); commissionController.clear(); - selectedPaymentModeId = null; + // selectedPaymentModeId = null; // why this command }); }, child: Container( @@ -2768,7 +2877,7 @@ class _endorsomentValidationState extends ConsumerState { // 🔥 Clear old values premiumController.clear(); commissionController.clear(); - selectedPaymentModeId = null; + // selectedPaymentModeId = null; }); }, child: Container( @@ -3136,11 +3245,10 @@ class _endorsomentValidationState extends ConsumerState { isPolicyNumberReadOnly = true; } - // 🔥 CLEAR FIELDS WHEN SWITCHING + // Clear only fields that are not relevant to Internal. if (isInternal) { selectedInsurer = null; selectedBroker = null; - selectedPaymentModeId = null; insuredNameController.clear(); regNoController.clear(); @@ -3228,6 +3336,7 @@ class _endorsomentValidationState extends ConsumerState { } Widget buildPaymentModeDropdown() { + print("selectedPaymentModeId======>$selectedPaymentModeId"); Map? selectedItem = getPaymentModeData .where( @@ -3241,6 +3350,21 @@ class _endorsomentValidationState extends ConsumerState { ) : null; + // Fallback: if id does not match, resolve by saved text value. + if (selectedItem == null && + selectedPaymentModeValue != null && + selectedPaymentModeValue!.trim().isNotEmpty) { + final normalizedSelected = selectedPaymentModeValue!.trim().toLowerCase(); + final matches = getPaymentModeData.where((item) { + final value = item['value']?.toString().trim().toLowerCase() ?? ''; + return value == normalizedSelected; + }); + if (matches.isNotEmpty) { + selectedItem = matches.first; + selectedPaymentModeId = selectedItem['id']?.toString(); + } + } + return buildCommonDropdown( label: "Payment Mode", hintText: "Select Payment Mode", @@ -3249,7 +3373,12 @@ class _endorsomentValidationState extends ConsumerState { selectedItem: selectedItem, /// 🔥 IMPORTANT FIX HERE - itemAsString: (val) => val['value'] ?? "", + itemAsString: (val) => + val['value']?.toString() ?? + val['payment_mode_value']?.toString() ?? + val['payment_mode']?.toString() ?? + val['name']?.toString() ?? + "", isFocused: isPaymentModeFocused, onFocusChanged: (val) { @@ -3259,6 +3388,11 @@ class _endorsomentValidationState extends ConsumerState { if (val != null) { setState(() { selectedPaymentModeId = val['id'].toString(); + selectedPaymentModeValue = (val['value'] ?? + val['payment_mode_value'] ?? + val['payment_mode'] ?? + val['name']) + ?.toString(); }); } }, diff --git a/lib/presentation/screens/Enquiry/policy_claims_endros/endrosment.dart b/lib/presentation/screens/Enquiry/policy_claims_endros/endrosment.dart index e94ab9c..9df7d45 100644 --- a/lib/presentation/screens/Enquiry/policy_claims_endros/endrosment.dart +++ b/lib/presentation/screens/Enquiry/policy_claims_endros/endrosment.dart @@ -968,8 +968,17 @@ class endosementState extends ConsumerState { Expanded( flex: 3, - child: - Text(item['policy_number'] ?? '-', style: _dataBold)), + child: Tooltip( + message: _policyNumberHoverText( + item['policy_number'], + item['policy_from'], + ), + child: Text( + item['policy_number'] ?? '-', + style: _dataBold, + overflow: TextOverflow.ellipsis, + ), + )), Expanded( flex: 2, @@ -1107,19 +1116,30 @@ class endosementState extends ConsumerState { ), // --- DOWNLOAD --- - _actionIconButton( - context: context, - icon: Icons.download_rounded, - tooltip: 'Download', - iconColor: Colors.blue.shade600, - hoverColor: Colors.blue.shade50, // ✅ red tint on hover for delete - onTap: () => apiService.downloadFile( - apiUrl: - 'endorsement/downloadEndorsementCompletionFile?id=$id', - apiId: id, - localFile: null, - fileName: fileName, - ), + // --- DOWNLOAD BUTTON --- + Builder( + builder: (context) { + final String? uploadedFile = item['endorsement_completion_file']; + final String? uploadedFileName = (uploadedFile != null && uploadedFile.isNotEmpty) + ? uploadedFile.split('/').last + : null; + + if (uploadedFileName == null) return const SizedBox.shrink(); + + return _actionIconButton( + context: context, + icon: Icons.download_rounded, + tooltip: 'Download Endorsement Document', + iconColor: Colors.blue.shade600, + hoverColor: Colors.blue.shade50, + onTap: () => apiService.downloadFile( + apiUrl: 'endorsement/downloadEndorsementCompletionFile?id=${item['id']}&type=completion', + apiId: item['id'].toString(), + localFile: null, + fileName: uploadedFileName, + ), + ); + }, ), // --- DELETE (Accounts only) --- @@ -2200,6 +2220,22 @@ class endosementState extends ConsumerState { ); } + /* + * Hover text format: + * "INHY/00009887 (internal)" or "0123489945666 (external)". + */ + String _policyNumberHoverText(dynamic policyNumber, dynamic policyFrom) { + final String number = (policyNumber?.toString().trim().isNotEmpty ?? false) + ? policyNumber.toString().trim() + : '-'; + final String source = policyFrom?.toString().trim().toLowerCase() ?? ''; + + if (source == 'internal' || source == 'external') { + return '$number ($source)'; + } + return number; + } + int _calculatePendingDays(String? createdAt) { if (createdAt == null || createdAt.isEmpty) return 0; try { @@ -2212,13 +2248,41 @@ class endosementState extends ConsumerState { } String formatDateForList(String? dateStr) { - if (dateStr == null || dateStr.isEmpty || dateStr == '-') return '-'; + /* + * Normalize all non-usable date values for UI. + * Required output: "-" for Internal/Invalid/null/empty/zero-range dates. + */ + if (dateStr == null) return '-'; + final String normalized = dateStr.trim(); + if (normalized.isEmpty || normalized == '-') return '-'; + if (normalized.toLowerCase() == 'internal') return '-'; + if (normalized.toLowerCase() == 'invalid date') return '-'; try { - // Catch invalid dates like "0000-00-00" or "30-11--0001" - if (dateStr.contains('0000') || dateStr.startsWith('30-11--')) return '-'; + /* + * Catch zero/invalid ranges: + * - 0000-00-00 + * - dd-00-yyyy / dd-mm-0000 + * - malformed historical invalid strings. + */ + if (normalized.contains('0000') || + normalized.contains('-00-') || + normalized.startsWith('00-') || + normalized.endsWith('-00') || + normalized.startsWith('30-11--')) { + return '-'; + } - DateTime parsed = DateTime.parse(dateStr); + DateTime parsed; + /* + * API commonly sends date as dd-MM-yyyy (e.g. 05-12-2025), + * while some flows may send ISO (yyyy-MM-dd). Support both. + */ + if (RegExp(r'^\d{2}-\d{2}-\d{4}$').hasMatch(normalized)) { + parsed = DateFormat('dd-MM-yyyy').parseStrict(normalized); + } else { + parsed = DateTime.parse(normalized); + } // Validate year is reasonable if (parsed.year < 2000 || parsed.year > 2100) return '-'; @@ -2275,12 +2339,12 @@ class endosementState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ // ── UPLOAD LABEL ────────────────────────────── - Text('Upload Document', style: GoogleFonts.poppins(fontSize: 11, fontWeight: FontWeight.w500)), + // Text('Upload Endorsement Document', style: GoogleFonts.poppins(fontSize: 11, fontWeight: FontWeight.w500)), const SizedBox(height: 6), // ── UPLOAD FIELD ────────────────────────────── ThemedUploadField( - hintText: selectedFileNames ?? "Upload Document", + hintText: selectedFileNames ?? "Upload Endorsement Document", padHorizontal: 4, padVertical: 5, fontSZ: 11, @@ -2360,7 +2424,7 @@ class endosementState extends ConsumerState { Icon(Icons.info_outline, color: Colors.orange.shade400, size: 14), const SizedBox(width: 6), Text( - 'No file uploaded yet', + 'No Endorsement file uploaded yet', style: GoogleFonts.poppins(fontSize: 11, color: Colors.orange.shade700), ), ], diff --git a/lib/presentation/screens/Grid/gridList.dart b/lib/presentation/screens/Grid/gridList.dart new file mode 100644 index 0000000..c9adca9 --- /dev/null +++ b/lib/presentation/screens/Grid/gridList.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'package:nhance_partner/core/routing/routes.dart'; +import 'package:nhance_partner/core/services/api_service.dart'; +import 'package:nhance_partner/data/utils/Pagination.dart'; +import 'package:nhance_partner/presentation/layouts/main_layout.dart'; +import 'package:nhance_partner/presentation/providers/userRoleProvider.dart'; +import 'gridUpload.dart'; +import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart'; + +class GridListScreen extends ConsumerStatefulWidget { + const GridListScreen({super.key}); + + @override + ConsumerState createState() => _GridListScreenState(); +} + +class _GridListScreenState extends ConsumerState { + final ApiService _apiService = ApiService(); + final TextEditingController _searchController = TextEditingController(); + + int _currentPage = 1; + int _itemsPerPage = 10; + bool _isLoading = false; + + List> _allRows = []; + List> _filteredRows = []; + + bool get _showUploadButton { + final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); + return role == 'manager' || role == 'accounts'; + } + + @override + void initState() { + super.initState(); + Future.microtask(_loadGridList); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadGridList() async { + setState(() => _isLoading = true); + try { + final response = await _apiService.fetchGridFileList(); + + if ((response['status'] ?? '').toString().toLowerCase() == 'success') { + final rows = List>.from(response['data'] ?? const []); + setState(() { + _allRows = rows; + _filteredRows = List>.from(rows); + _currentPage = 1; + }); + } else { + setState(() { + _allRows = []; + _filteredRows = []; + }); + } + } catch (_) { + setState(() { + _allRows = []; + _filteredRows = []; + }); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _filterRows(String query) { + final q = query.trim().toLowerCase(); + setState(() { + _currentPage = 1; + if (q.isEmpty) { + _filteredRows = List>.from(_allRows); + return; + } + _filteredRows = _allRows.where((item) { + return (item['id'] ?? '').toString().toLowerCase().contains(q) || + (item['incentive_file_name'] ?? '').toString().toLowerCase().contains(q) || + (item['vaild_from'] ?? item['valid_from'] ?? '') + .toString() + .toLowerCase() + .contains(q) || + (item['created_by_name'] ?? '').toString().toLowerCase().contains(q) || + (item['created_date'] ?? '').toString().toLowerCase().contains(q) || + (item['created_at'] ?? '').toString().toLowerCase().contains(q); + }).toList(); + }); + } + + List> get _pageRows { + final list = List>.from(_filteredRows); + list.sort((a, b) { + final left = int.tryParse(a['id']?.toString() ?? '') ?? 0; + final right = int.tryParse(b['id']?.toString() ?? '') ?? 0; + return right.compareTo(left); + }); + if (list.isEmpty) return []; + + final maxPage = (list.length / _itemsPerPage).ceil(); + final safePage = _currentPage.clamp(1, maxPage); + final start = (safePage - 1) * _itemsPerPage; + final end = (start + _itemsPerPage).clamp(0, list.length); + return list.sublist(start, end); + } + + @override + Widget build(BuildContext context) { + return MainLayout( + title: 'Grid List', + body: SelectionArea( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Row( + children: [ + Text('Grid List', style: _titleStyle), + const Spacer(), + ThemedSearchField( + hintText: 'Search', + backgroundColor: Colors.white, + txtHeight: 34, + txtwidth: MediaQuery.of(context).size.width * 0.18, + controller: _searchController, + onChanged: _filterRows, + ), + const SizedBox(width: 10), + if (_showUploadButton) + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2E7D6E), + foregroundColor: Colors.white, + ), + onPressed: () async { + final shouldRefresh = await showDialog( + context: context, + builder: (_) => const UploadGridModal(), + ); + if (shouldRefresh == true && mounted) { + await _loadGridList(); + } + }, + icon: const Icon(Icons.add, size: 18), + label: const Text('Upload Grid'), + ), + ], + ), + const SizedBox(height: 10), + _buildHeader(), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _pageRows.isEmpty + ? const Center(child: Text('No available data')) + : ListView.builder( + itemCount: _pageRows.length, + itemBuilder: (context, index) { + final row = _pageRows[index]; + final sno = ((_currentPage - 1) * _itemsPerPage) + index + 1; + return _buildRow(context, row, sno); + }, + ), + ), + PaginationControls( + currentPage: _currentPage, + itemsPerPage: _itemsPerPage, + totalItems: _filteredRows.length, + onPageChanged: (page) => setState(() => _currentPage = page), + onItemsPerPageChanged: (items) => setState(() { + _itemsPerPage = items; + _currentPage = 1; + }), + ), + ], + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + child: Row( + children: [ + Expanded(flex: 2, child: Text('Valid From', style: _headerStyle)), + Expanded(flex: 4, child: Text('File Name', style: _headerStyle)), + Expanded(flex: 2, child: Text('Created By', style: _headerStyle)), + Expanded(flex: 2, child: Text('Created Date', style: _headerStyle)), + Expanded(flex: 1, child: Text('Action', style: _headerStyle)), + ], + ), + ); + } + + Widget _buildRow(BuildContext context, Map row, int sno) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + decoration: const BoxDecoration( + color: Colors.white, + border: Border(bottom: BorderSide(color: Colors.blueGrey, width: 0.15)), + ), + child: Row( + children: [ + Expanded( + flex: 2, + child: Text( + (row['vaild_from'] ?? row['valid_from'] ?? '-').toString(), + style: _dataStyle, + ), + ), + Expanded( + flex: 4, + child: Text( + (row['incentive_file_name'] ?? '-').toString(), + style: _dataStyle, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + Expanded( + flex: 2, + child: Text((row['created_by_name'] ?? '-').toString(), style: _dataStyle), + ), + Expanded( + flex: 2, + child: Text((row['created_date'] ?? '-').toString(), style: _dataStyle), + ), + Expanded( + flex: 1, + child: IconButton( + tooltip: 'View', + onPressed: () => + context.go('${AppRoutes.gridView}?file_id=${row['id']}'), + icon: const Icon(Icons.visibility_outlined, size: 18), + ), + ), + ], + ), + ); + } +} + +class GridList extends GridListScreen { + const GridList({super.key}); +} + +final _titleStyle = GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w600); +final _dataStyle = GoogleFonts.inter(fontSize: 12, fontWeight: FontWeight.w400); +final _headerStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: const Color(0xFF1E293B), +); diff --git a/lib/presentation/screens/Grid/gridUpload.dart b/lib/presentation/screens/Grid/gridUpload.dart new file mode 100644 index 0000000..c40c897 --- /dev/null +++ b/lib/presentation/screens/Grid/gridUpload.dart @@ -0,0 +1,884 @@ +// import 'dart:io' as html; +// import 'dart:nativewrappers/_internal/vm/lib/typed_data_patch.dart'; +import 'dart:convert'; +import 'dart:typed_data'; // Import for Uint8List +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'package:nhance_partner/data/utils/toastNotification.dart'; +import 'package:nhance_partner/presentation/themes/indicators/text_field_theme.dart'; +import 'package:toastification/toastification.dart'; +import 'package:universal_html/html.dart' as html; + +import 'package:nhance_partner/core/config/env.dart'; +import 'package:nhance_partner/core/services/api_service.dart'; +import 'package:nhance_partner/data/services/auth_service.dart'; +import 'package:nhance_partner/data/utils/validators.dart'; +import 'package:nhance_partner/presentation/layouts/main_layout.dart'; +import 'package:nhance_partner/presentation/providers/manager_provider.dart'; +import 'package:nhance_partner/presentation/themes/indicators/customizd_file_upload.dart'; +import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart'; +import 'package:nhance_partner/presentation/themes/indicators/month_field_theme.dart'; +import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart'; +// import '../../../themes/indicators/upload_doc_theme.dart'; + +/// function to open the modal +void showUploadGridModal(BuildContext context) { + showDialog( + context: context, + builder: (context) => const UploadGridModal(), + ); +} + +class UploadGridModal extends ConsumerStatefulWidget { + const UploadGridModal({super.key}); + + @override + ConsumerState createState() => + UploadGridModalState(); +} + +class UploadGridModalState extends ConsumerState { + late ApiService apiService; + bool isLoading = false; + final _formKey = GlobalKey(); + final GlobalKey>> dropDownKey = + GlobalKey>>(); + + List> getAgentData = []; + List> filteredData = []; + List> getAgentGridFileData = []; + List> filteredGridData = []; + Map controllers = {}; + + // html.File? passportFile; + List selectedFiles = []; + String? selectedFileNames; + + List tabHeader = ['name', 'agentId', 'date', 'search']; + String? _token; + dynamic userId; + dynamic managerId; + dynamic agentId; + DateTime? _validFrom; + + String _formatDateForApi(DateTime date) => + date.toIso8601String().split('T').first; + + String _formatDateForUi(DateTime date) { + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + return '${date.day} ${months[date.month - 1]} ${date.year}'; + } + + Future _pickValidFrom() async { + final today = DateTime.now(); + final minDate = DateTime(today.year, today.month, today.day); + final safeInitial = (_validFrom != null && _validFrom!.isAfter(minDate)) + ? _validFrom! + : minDate; + final picked = await showDatePicker( + context: context, + firstDate: minDate, + lastDate: DateTime(2100), + initialDate: safeInitial, + ); + if (picked == null) return; + setState(() => _validFrom = picked); + // Backend expects `incentive_month` but payout-grid upload uses a full date. + controllers['date']?.text = _formatDateForApi(picked); + } + Map dataDetails() { + final selectedDate = controllers["date"]?.text; + final data = { + "agent_id": agentId, + "incentive_month": selectedDate, + "created_by": userId, + "file_type": "grid", + }; + return data; + } + + @override + void initState() { + super.initState(); + apiService = ApiService(); + for (String field in tabHeader) { + controllers[field] = TextEditingController(); + } + Future.microtask(_loadInitialModalData); + + _initializeToken(); + // getAgentList(); + } + + Future _loadInitialModalData() async { + managerId = ref.read(managerIdProvider); + userId = ref.read(userIdProvider); + agentId = null; + + if (managerId != null) { + await getAgentList(managerId); + } + + // Always fetch history list when modal opens. + // if (agentId != null) { + // await getAgentIncenctiveFileList(agentId); + // } + } + + Future _initializeToken() async { + _token = await AuthService.getToken(); + print("APISERTOKEN - $_token"); + } + + @override + void dispose() { + for (var controller in controllers.values) { + controller.dispose(); + } + controllers['date']?.dispose(); + controllers['code']?.dispose(); + super.dispose(); + } + + Future refresh() async { + setState(() { + // reset file fields + selectedFiles = []; + + // selectedFileNames = ""; + selectedFileNames = null; + + // getAgentData = []; + // filteredData = []; + getAgentGridFileData = []; + filteredGridData = []; + + // clear all text controllers + for (var controller in controllers.values) { + controller.clear(); + } + _validFrom = null; + dropDownKey.currentState?.clear(); // 👈 clear selected agent + controllers['agentId']?.clear(); // clear text field value too + controllers['uploadFile']?.clear(); + }); + + // Re-fetch history after clearing UI fields. + // if (agentId != null) { + // await getAgentIncenctiveFileList(agentId); + // } + } + + void filterData(String query) { + print("FilterDAta - $query"); + setState(() { + filteredGridData = getAgentGridFileData.where((item) { + return (item['incentive_month'] ?? '-').toLowerCase().contains( + query.toLowerCase(), + ); + }).toList(); + }); + } + + Future getAgentList(int id) async { + print('getClaimList called'); + setState(() { + isLoading = true; + }); + + try { + final response = await apiService.fetchAgentUserList(managerId); + + if (response['status'] == 'success') { + print('getAgentListData - ${response['data']}'); + setState(() { + getAgentData = List>.from(response['data']); + print('API Data - $getAgentData'); + + filteredData = List.from(getAgentData); + // print('originalData - $filteredData'); + }); + } else { + getAgentData = []; + filteredData = []; + } + } catch (e) { + print('Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + Future getAgentIncenctiveFileList(agentId) async { + print('getClaimList agentId - $agentId'); + setState(() { + isLoading = true; + }); + + try { + final response = await apiService.fetchAgentIncentiveList(agentId, type: 'grid'); + + if (response['status'] == 'success') { + print('AgentIncentive - ${response['data']}'); + setState(() { + getAgentGridFileData = List>.from( + response['data'], + ); + print('API AgentIncentive - $getAgentGridFileData'); + + filteredGridData = List.from(getAgentGridFileData); + // print('originalData - $filteredGridData'); + }); + } else { + getAgentGridFileData = []; + } + } catch (e) { + print('Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + Future handleSave() async { + final dateText = controllers['date']?.text.trim() ?? ''; + if (dateText.isEmpty) { + ToastHelper.showErrorToast(context, 'Select valid from date'); + return; + } + if (!_formKey.currentState!.validate()) return; + print("Handl1"); + final dataSet = dataDetails(); + print("dataSetAgent - $dataSet"); + print("Handl13f"); + + if (selectedFiles.isEmpty) { + print("❌ No file selected!"); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Please select a file before submitting"), + ), + ); + return; + } + + final invalidFiles = selectedFiles.where((file) { + final name = file.name.toLowerCase(); + return !(name.endsWith('.xls') || name.endsWith('.xlsx')); + }).toList(); + if (invalidFiles.isNotEmpty) { + ToastHelper.showErrorToast( + context, + 'Upload only Excel files (.xls, .xlsx)', + ); + return; + } + + setState(() { + uploadIncentiveData(dataSet); + }); + + print("Handl2"); + + } + + Future uploadIncentiveData(Map dataSet) async { + final uri = Uri.parse('${Env.apiUrl}grid/upload'); + + if (_token == null) { + throw Exception('Token not found. Please log in.'); + } + // Use MultipartRequest (POST only) + final request = http.MultipartRequest('POST', uri); + request.headers['Authorization'] = 'Bearer $_token'; + request.headers['app-signature'] = Env.App_Signature; + + print("USerDAta - $dataSet"); + + // dataSet.forEach((key, value) { + // request.fields[key] = value.toString(); + // print("✅ Encoded travel_details2: ${request.fields[key]}"); + // }); + + dataSet.forEach((key, value) { + if (key != 'incentive_file_name') { + request.fields[key] = value.toString(); + print("✅ Encoded $key: ${request.fields[key]}"); + } + }); + + if (selectedFiles.isNotEmpty) { + for (final file in selectedFiles) { + try { + if (file.bytes != null) { + final multipartFile = http.MultipartFile.fromBytes( + 'incentive_file_name', + file.bytes!, + filename: file.name, + ); + request.files.add(multipartFile); + } else if (file.path != null) { + final multipartFile = await http.MultipartFile.fromPath( + 'incentive_file_name', + file.path!, + filename: file.name, + ); + request.files.add(multipartFile); + } + print("📎 File attached: ${file.name}"); + } catch (e) { + print("❌ Failed to attach file ${file.name}: $e"); + } + } + } + + // Attach file if selected + // if (passportFile != null) { + // try { + // final reader = html.FileReader(); + // reader.readAsArrayBuffer(passportFile! as html.Blob); + // await reader.onLoad.first; + // + // final data = reader.result as Uint8List; + // + // final multipartFile = http.MultipartFile.fromBytes( + // 'incentive_file_name', + // data, + // filename: passportFile!.name, + // ); + // + // request.files.add(multipartFile); + // print("📎 File attached: ${passportFile!.name}"); + // } catch (e) { + // print("❌ Failed to read file: $e"); + // } + // } else { + // print("⚠️ No passport file selected."); + // } + + print("🚀 Sending request with fields: ${request.fields}"); + + try { + final streamedResponse = await request.send(); + final response = await http.Response.fromStream(streamedResponse); + print("Response status: ${response.statusCode}"); + print("Response body: ${response.body}"); + final responseBody = json.decode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + // dispose(); + print("✅ Partner submitted successfully!"); + + print("📨 Response: ${response.body}"); + + final data = dataDetails(); + final agentId = null; + + final status = responseBody['status']; + final message = responseBody['data'] ?? ''; + + if (status == 'success') { + ToastHelper.showSuccessToast(context, 'File uploaded successfully!'); + // await getAgentIncenctiveFileList(agentId); + // Close the modal and signal the parent to refresh. + Navigator.pop(context, true); + } else { + ToastHelper.showErrorToast( + context, + message.isNotEmpty ? message : 'Something went wrong!', + ); + } + + // context.go(AppRoutes.agentLst); + } else if (response.statusCode == 403) { + await apiService.clearLocalStorageAndRedirect(); + } else { + print("❌ Submission failed. Status: ${response.statusCode}"); + print("📨 Body: ${response.body}"); + String errorMessage = 'There was a problem in creating user. Please try again.'; + try { + if (responseBody['data'] is String && + (responseBody['data'] as String).isNotEmpty) { + final parsedData = json.decode(responseBody['data']); + if (parsedData is Map && + parsedData['message'] != null) { + errorMessage = parsedData['message'].toString(); + } + } else if (responseBody['message'] != null) { + errorMessage = responseBody['message'].toString(); + } + } catch (_) { + // keep fallback message + } + + ToastHelper.showErrorToast(context, errorMessage); + } + } catch (e) { + print("🔥 Error submitting user: $e"); + } + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final isCompact = screenWidth < 1000; + final dialogWidth = isCompact ? screenWidth * 0.95 : screenWidth * 0.55; + final fieldWidth = isCompact ? dialogWidth * 0.9 : screenWidth * 0.27; + + return SelectionArea( + child:AlertDialog( + backgroundColor: Colors.white, + content: Container( + width: dialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "Grid Files", + style: GoogleFonts.poppins( + fontSize: 14, + color: Color(0xFF50A398), + fontWeight: FontWeight.w500, + ), + ), + Spacer(), + Tooltip( + message: 'Refresh', + child: IconButton( + icon: const Icon( + Icons.refresh, + size: 18, + color: Color(0xFF2E7D6E), + // color: Color(0xFF425B5B), + ), + onPressed: () { + refresh(); + }, + splashRadius: 28, + hoverColor: Colors.black12, + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(), + ), + ), + Tooltip( + message: 'Close', + child: IconButton( + icon: const Icon( + Icons.close, + size: 18, + // color: Color(0xFF425B5B), + color: Color(0xFF2E7D6E), + ), + onPressed: () { + Navigator.pop(context); + }, + splashRadius: 28, + hoverColor: Colors.black12, + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + + children: [ + Form( + key: _formKey, + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Valid From', + style: _labelStyle, + ), + SizedBox(height: 5), + SizedBox( + width: MediaQuery.of(context).size.width * 0.27, + height: 40, + child: InkWell( + onTap: _pickValidFrom, + borderRadius: BorderRadius.circular(6), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all(color: const Color(0xFFE2E8F0)), + color: Colors.white, + ), + child: Row( + children: [ + Expanded( + child: Text( + _validFrom == null + ? 'Select Valid From' + : _formatDateForUi(_validFrom!), + style: GoogleFonts.inter( + fontSize: 12, + color: _validFrom == null + ? const Color(0xFFCBD5E1) + : Colors.black, + ), + ), + ), + const Icon( + Icons.calendar_today_outlined, + size: 16, + color: Color(0xFF94A3B8), + ), + ], + ), + ), + ), + ), + ], + ), + SizedBox(width: 15), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Upload Files', style: _labelStyle), + ], + ), + SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ThemedUploadField( + key: ValueKey(selectedFileNames), + allowMultiple: true, + // hintText: "Upload Document", + txtheight: 40, + // validator: (value) => Validators.requiredField( + // value, + // "Upload Document", + // ), + // validator: (value) { + // if (passportFile == null) { + // return "Please upload a file"; + // } + // return null; + // }, + // backgroundColor: Color(0xFFECECEC), + backgroundColor: Colors.white, + borderColor: Color(0xFFE2E8F0), + allowedExtensions: const ['xls', 'xlsx'], + hintText: + (selectedFileNames == null || + selectedFileNames!.isEmpty) + ? "Upload Document" + : selectedFileNames, + txtwidth: isCompact + ? fieldWidth * 0.71 + : MediaQuery.of(context).size.width * 0.19, + onFilesSelected: (fileNames, files) { + print("Files picked: ${fileNames.join(', ')}"); + // print("Size: ${file.size}"); + // print( + // "Path: ${file.path}", + // ); // works on mobile/desktop + // print("Bytes: ${file.bytes}"); + setState(() { + selectedFiles = files; + selectedFileNames = fileNames.join(', '); + }); + }, + ), + SizedBox(width: 15), + Container( + width: isCompact + ? fieldWidth * 0.25 + : MediaQuery.of(context).size.width * 0.068, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + // backgroundColor: Color(0xFF425B5B), + backgroundColor: Color(0xFF2E7D6E), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 6, + ), // 👈 reduce radius (default ~20) + ), + ), + + onPressed: () { + handleSave(); + // Navigator.pop(context); + }, + child: Text( + "Submit", + style: GoogleFonts.poppins(fontSize: 12), + ), + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + // fILE LIST section + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text('Files', style: _labelHeaderStyle), + // ThemedSearchField( + // hintText: 'Search', + // backgroundColor: Colors.white, + // // backgroundColor: Color(0xFFECECEC), + // onChanged: filterData, + // // onChanged: null, + // controller: controllers['search']!, + // txtwidth: MediaQuery.of(context).size.width * 0.15, + // txtHeight: 30, + // ), + // ], + // ), + // const SizedBox(height: 10), + // + // Expanded( + // child: isLoading + // ? const Center(child: CircularProgressIndicator()) + // : filteredGridData.isEmpty + // ? const Center(child: Text("No files found")) + // : LayoutBuilder( + // builder: (context, constraints) { + // final fileAreaWidth = constraints.maxWidth; + // final crossAxisCount = + // fileAreaWidth < 520 ? 1 : 2; + // const spacing = 10.0; + // final tileWidth = (fileAreaWidth - + // spacing * (crossAxisCount - 1) - + // 16) / + // crossAxisCount; + // const targetTileHeight = 70.0; + // final aspectRatio = (tileWidth / targetTileHeight) + // .clamp(3.2, 12.0); + // + // return GridView.builder( + // padding: const EdgeInsets.all(8), + // gridDelegate: + // SliverGridDelegateWithFixedCrossAxisCount( + // crossAxisCount: crossAxisCount, + // crossAxisSpacing: spacing, + // mainAxisSpacing: spacing, + // childAspectRatio: aspectRatio, + // ), + // itemCount: filteredGridData.length, + // itemBuilder: (context, index) { + // final file = filteredGridData[index]; + // return IncentiveFileRow( + // fileName: + // file['incentive_file_name'] ?? "Unknown", + // date: file['incentive_month'] ?? "-", + // onUpload: () { + // print("Upload ${file['id']}"); + // final selectedId = file['id']; + // final path = + // 'agent/downloadAgentIncentiveFile?type=grid&id=$selectedId'; + // apiService.getPdfDownload(path, selectedId); + // }, + // onDelete: () async { + // print("Delete ${file['id']}"); + // final selectedId = file['id']; + // final response = await apiService + // .deleteAgentIncentiveFile(selectedId); + // print("res - $response"); + // + // if (response['status'] == 'success') { + // print("DELTED"); + // + // final data = + // dataDetails(); // Map + // final agentId = null; + // + // // getAgentIncenctiveFileList(agentId); + // } else { + // // Fluttertoast.showToast( + // // msg: "Something went wrong", + // // toastLength: Toast.LENGTH_SHORT, + // // gravity: ToastGravity.BOTTOM, + // // ); + // } + // }, + // ); + // }, + // ); + // }, + // ), + // ), + ], + ), + ), + ),); + } +} + +final _labelStyle = GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 12, +); +final _labelHeaderStyle = GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, +); + +class IncentiveFileRow extends StatelessWidget { + final String fileName; + final String date; + final VoidCallback? onUpload; + final VoidCallback? onDelete; + + const IncentiveFileRow({ + super.key, + required this.fileName, + required this.date, + this.onUpload, + this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE3E3E3)), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6.0), + decoration: BoxDecoration( + color: const Color(0xFFF4F6F8), + borderRadius: BorderRadius.circular(6.0), + border: Border.all(color: const Color(0xFFE3E3E3)), + ), + child: const Icon( + Icons.file_present, + color: Color(0xFF838587), + size: 18, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + fileName, + style: GoogleFonts.poppins( + fontWeight: FontWeight.w400, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(height: 2), + Text( + date, + style: GoogleFonts.poppins( + color: const Color(0xFF6E6E6E), + fontSize: 10, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onUpload, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon( + Icons.file_download_outlined, + color: Color(0xFF6E6E6E), + size: 20, + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onDelete, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon( + Icons.delete_outlined, + color: Color(0xFF6E6E6E), + size: 20, + ), + ), + ), + ), + ], + ), + ); + } +} + +class GridUploadScreen extends StatelessWidget { + const GridUploadScreen({super.key}); + + @override + Widget build(BuildContext context) { + return MainLayout( + title: 'Grid Upload', + body: Center( + child: ElevatedButton.icon( + onPressed: () => showUploadGridModal(context), + icon: const Icon(Icons.upload_file_outlined), + label: const Text('Open Grid Upload'), + ), + ), + ); + } +} diff --git a/lib/presentation/screens/Grid/gridView.dart b/lib/presentation/screens/Grid/gridView.dart new file mode 100644 index 0000000..10c2320 --- /dev/null +++ b/lib/presentation/screens/Grid/gridView.dart @@ -0,0 +1,5410 @@ +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:jwt_decode/jwt_decode.dart'; +import 'package:nhance_partner/core/routing/routes.dart'; +import 'package:nhance_partner/core/services/api_service.dart'; +import 'package:nhance_partner/data/services/auth_service.dart'; +import 'package:nhance_partner/data/utils/Pagination.dart'; +import 'package:nhance_partner/data/utils/toastNotification.dart'; +import 'package:nhance_partner/presentation/layouts/main_layout.dart'; +import 'package:nhance_partner/presentation/layouts/responsive_layout.dart'; +import 'package:nhance_partner/presentation/providers/userRoleProvider.dart'; +// import 'package:nhance_partner/presentation/screens/Grid/Gridupload.dart'; +import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart'; +import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart'; + +class GridViewScreen extends StatelessWidget { + const GridViewScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const gridViewScreen(); + } +} + +class gridViewScreen extends ConsumerStatefulWidget { + const gridViewScreen({super.key}); + + @override + ConsumerState createState() => _gridViewScreenState(); +} + +class _gridViewScreenState extends ConsumerState { + final ApiService _apiService = ApiService(); + String? _loggedId; + + bool _isLoading = false; + + // API response + List> _grid = []; + List> _filteredGrid = []; + + List _insurers = []; + List> _planTypes = []; + List _rtos = []; + List _vehicleTypes = []; + List _segments = []; + + // Filters + String? _selectedInsurer; + String? _selectedRto; + String? _selectedVehicleType; + String? _selectedSegment; + String? _selectedPlanTypeValue; // comp/tp/od + + final TextEditingController _searchController = TextEditingController(); + final TextEditingController _editVehicleTypeController = + TextEditingController(); + final TextEditingController _editFuelController = TextEditingController(); + final TextEditingController _editRtoController = TextEditingController(); + final TextEditingController _editSegmentController = TextEditingController(); + final TextEditingController _editBrokerController = TextEditingController(); + final TextEditingController _editCompController = TextEditingController(); + final TextEditingController _editTpController = TextEditingController(); + final TextEditingController _editOdController = TextEditingController(); + final TextEditingController _editBrokerCompController = + TextEditingController(); + final TextEditingController _editBrokerTpController = TextEditingController(); + final TextEditingController _editBrokerOdController = TextEditingController(); + final TextEditingController _editPartnerCompController = + TextEditingController(); + final TextEditingController _editPartnerTpController = + TextEditingController(); + final TextEditingController _editPartnerOdController = + TextEditingController(); + final TextEditingController _editRemarksController = TextEditingController(); + + // Pagination + int _currentPage = 1; + int _itemsPerPage = 10; + bool _isUpdating = false; + String _retentionRate = '-'; + + @override + void dispose() { + _searchController.dispose(); + _editVehicleTypeController.dispose(); + _editFuelController.dispose(); + _editRtoController.dispose(); + _editSegmentController.dispose(); + _editBrokerController.dispose(); + _editCompController.dispose(); + _editTpController.dispose(); + _editOdController.dispose(); + _editBrokerCompController.dispose(); + _editBrokerTpController.dispose(); + _editBrokerOdController.dispose(); + _editPartnerCompController.dispose(); + _editPartnerTpController.dispose(); + _editPartnerOdController.dispose(); + _editRemarksController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _loadRetentionRateFromToken(); + _fetch(); + } + + Future _loadRetentionRateFromToken() async { + try { + final token = await AuthService.getToken(); + if (token == null || token.isEmpty) return; + + final decodedToken = Jwt.parseJwt(token); + final data = decodedToken['data']; + if (data is! Map) return; + + final retentionRate = data['retention_rate']?.toString(); + final loggedId = data['id']?.toString(); + if (retentionRate == null || retentionRate.trim().isEmpty) return; + + if (mounted) { + setState(() { + _loggedId = (loggedId == null || loggedId.trim().isEmpty) + ? null + : loggedId.trim(); + _retentionRate = retentionRate.trim(); + _grid = _applyRetentionToGridRows(_grid); + _filteredGrid = _applyRetentionToGridRows(_filteredGrid); + }); + } + } catch (_) { + // Keep fallback "-" when token parsing fails. + } + } + + String? _apiRoleFromAppRole(String? role) { + if (role == null) return null; + final r = role.trim(); + if (r.isEmpty) return null; + + switch (r.toLowerCase()) { + case 'manager': + return 'Manager'; + case 'handler': + return 'Handler'; + case 'staff': + return 'Staff'; + case 'agent': + return 'Agent'; + case 'accounts': + return 'Accounts'; + default: + return r; + } + } + + Future _fetch() async { + setState(() => _isLoading = true); + try { + final appRole = ref.read(userRoleProvider); + final apiRole = _apiRoleFromAppRole(appRole); + final queryParams = Uri.base.queryParameters; + final queryRole = queryParams['role']?.trim(); + final queryFileId = queryParams['file_id']?.trim(); + final effectiveRole = + (queryRole != null && queryRole.isNotEmpty) ? queryRole : apiRole; + String? effectiveFileId = + (queryFileId != null && queryFileId.isNotEmpty) ? queryFileId : null; + + if (effectiveFileId == null && + (effectiveRole?.toLowerCase() == 'manager' || + effectiveRole?.toLowerCase() == 'agent' || + effectiveRole?.toLowerCase() == 'accounts')) { + final fileListRes = await _apiService.fetchGridFileList(); + if ((fileListRes['status'] ?? '').toString().toLowerCase() == + 'success') { + final files = List>.from( + fileListRes['data'] ?? const [], + ); + if (files.isNotEmpty) { + files.sort((a, b) { + final left = int.tryParse(a['id']?.toString() ?? '') ?? 0; + final right = int.tryParse(b['id']?.toString() ?? '') ?? 0; + return right.compareTo(left); + }); + effectiveFileId = files.first['id']?.toString(); + } + } + } + + final res = await _apiService.loadPayoutGrid( + role: effectiveRole, + fileId: effectiveFileId, + ); + if ((res['status'] ?? '').toString().toLowerCase() != 'success') { + ToastHelper.showErrorToast( + context, + res['data']?.toString() ?? 'Failed to load payout grid', + ); + return; + } + + final payload = _extractPayload(res); + final grid = payload['grid']; + final insurers = payload['insurers']; + final planTypes = payload['plan_types']; + final rtos = payload['rtos']; + final vehicleTypes = payload['vehicle_types']; + final segments = payload['segments']; + + final rawGrid = (grid is List) + ? grid + .map>( + (e) => (e as Map).cast()) + .toList() + : >[]; + _grid = _applyRetentionToGridRows(rawGrid); + _filteredGrid = List>.from(_grid); + + _insurers = _toStringList(insurers); + _planTypes = _toPlanTypeList(planTypes); + _rtos = _toStringList(rtos); + _vehicleTypes = _toStringList(vehicleTypes); + _segments = _toStringList(segments); + + _applyFilters(); + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _setFilter(VoidCallback fn) { + fn(); + _applyFilters(); + } + + Map _extractPayload(Map res) { + final data = res['data']; + if (data is Map) return data; + if (data is Map) return data.cast(); + return res; + } + + List _toStringList(dynamic raw) { + if (raw is! List) return []; + return raw + .map((item) { + if (item is Map) { + for (final key in const [ + 'short_name', + 'name', + 'label', + 'title', + 'value', + 'rto', + 'segment', + 'vehicle_type', + ]) { + final val = item[key]?.toString().trim(); + if (val != null && val.isNotEmpty && val.toLowerCase() != 'null') { + return val; + } + } + final firstVal = item.values.isNotEmpty ? item.values.first : null; + return firstVal?.toString() ?? ''; + } + return item?.toString() ?? ''; + }) + .map((e) => e.trim()) + .where((e) => e.isNotEmpty && e.toLowerCase() != 'null') + .toSet() + .toList(); + } + + List> _toPlanTypeList(dynamic raw) { + if (raw is! List) return >[]; + return raw.map>((item) { + if (item is Map) { + final map = item.cast(); + final value = map['value']?.toString() ?? + map['id']?.toString() ?? + map['key']?.toString() ?? + ''; + final label = map['label']?.toString() ?? + map['name']?.toString() ?? + map['title']?.toString() ?? + map['value']?.toString() ?? + value; + return {'value': value, 'label': label}; + } + final v = item?.toString() ?? ''; + return {'value': v, 'label': v.toUpperCase()}; + }).where((e) => (e['value']?.toString().trim().isNotEmpty ?? false)).toList(); + } + + void _applyFilters() { + final q = _searchController.text.trim().toLowerCase(); + + final next = _grid.where((row) { + bool equalsOrAll(String? selected, dynamic raw) { + if (selected == null) return true; + return raw?.toString().toLowerCase() == selected.toLowerCase(); + } + + bool matchesPlanType() { + if (_selectedPlanTypeValue == null) return true; + final raw = row[_selectedPlanTypeValue]; + if (raw == null) return false; + final s = raw.toString().trim().toLowerCase(); + if (s.isEmpty || s == 'null' || s == '-') return false; + + // Treat 0 payout as "not configured" so it doesn't match plan filters. + final n = num.tryParse(s); + if (n != null) return n != 0; + + return s != '0'; + } + + bool matchesSearch() { + if (q.isEmpty) return true; + final values = [ + row['id']?.toString() ?? '', + row['insurer']?.toString() ?? '', + row['vehicle_type']?.toString() ?? '', + row['segment']?.toString() ?? '', + row['rto']?.toString() ?? '', + row['remarks']?.toString() ?? '', + row['comp']?.toString() ?? '', + row['partner_comp']?.toString() ?? '', + row['tp']?.toString() ?? '', + row['od']?.toString() ?? '', + ].map((e) => e.toLowerCase()); + + return values.any((v) => v.contains(q)); + } + + return equalsOrAll(_selectedInsurer, row['insurer']) && + equalsOrAll(_selectedVehicleType, row['vehicle_type']) && + equalsOrAll(_selectedSegment, row['segment']) && + equalsOrAll(_selectedRto, row['rto']) && + matchesPlanType() && + matchesSearch(); + }).toList(); + + setState(() { + _filteredGrid = next; + if (_showAddButton && !_hasAnyFilterSelection) { + _filteredGrid = []; + } + _currentPage = 1; + }); + } + + List> get _sortedGrid { + final next = [..._filteredGrid]; + next.sort((a, b) { + final ai = int.tryParse(a['id']?.toString() ?? '') ?? 0; + final bi = int.tryParse(b['id']?.toString() ?? '') ?? 0; + return bi.compareTo(ai); + }); + return next; + } + + List> get _paginatedGrid { + final sorted = _sortedGrid; + if (sorted.isEmpty) return []; + + final maxPage = (sorted.length / _itemsPerPage).ceil(); + final safePage = + _currentPage.clamp(1, maxPage == 0 ? 1 : maxPage) as int; + + final startIndex = (safePage - 1) * _itemsPerPage; + final endIndex = (startIndex + _itemsPerPage).clamp(0, sorted.length); + return sorted.sublist(startIndex, endIndex); + } + + String _nullableString(dynamic v) { + if (v == null) return '-'; + final s = v.toString().trim(); + if (s.isEmpty || s.toLowerCase() == 'null') return '-'; + return s; + } + + num? _toNum(dynamic value) { + if (value == null) return null; + final raw = value.toString().trim(); + if (raw.isEmpty || raw.toLowerCase() == 'null' || raw == '-') return null; + final normalized = raw.replaceAll(RegExp(r'[^0-9\.\-]'), ''); + if (normalized.isEmpty || normalized == '-' || normalized == '.') return null; + return num.tryParse(normalized); + } + + String _partnerValueFrom(dynamic baseValue) { + final base = _toNum(baseValue); + final retention = _toNum(_retentionRate); + if (base == null || retention == null) return '-'; + final result = base - retention; + if (result == result.roundToDouble()) { + return result.toInt().toString(); + } + return result.toStringAsFixed(2).replaceFirst(RegExp(r'0+$'), '').replaceFirst(RegExp(r'\.$'), ''); + } + + List> _applyRetentionToGridRows( + List> rows, + ) { + if (!_showAddButton) { + return rows + .map((row) => Map.from(row) + ..remove('partner_comp') + ..remove('partner_tp') + ..remove('partner_od')) + .toList(); + } + return rows.map((row) { + final next = Map.from(row); + next['partner_comp'] = _partnerValueFrom(next['comp']); + next['partner_tp'] = _partnerValueFrom(next['tp']); + next['partner_od'] = _partnerValueFrom(next['od']); + return next; + }).toList(); + } + + /// Edit column: Manager & Accounts only (not Agent). + bool get _showActionColumn { + final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); + return role == 'manager' || role == 'accounts'; + } + + bool get _showAddButton { + final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); + return role == 'agent'; + } + + bool get _showPartnerRetentionValues => _showAddButton; + bool get _showCompColumnForAgent => + !_showAddButton || + _selectedPlanTypeValue == 'comp'; + bool get _showTpColumnForAgent => + !_showAddButton || + _selectedPlanTypeValue == 'tp'; + bool get _showOdColumnForAgent => + !_showAddButton || + _selectedPlanTypeValue == 'od'; + + bool get _hasAnyFilterSelection { + return (_selectedInsurer != null && _selectedInsurer!.trim().isNotEmpty) || + (_selectedRto != null && _selectedRto!.trim().isNotEmpty) || + (_selectedVehicleType != null && + _selectedVehicleType!.trim().isNotEmpty) || + (_selectedSegment != null && _selectedSegment!.trim().isNotEmpty) || + (_selectedPlanTypeValue != null && + _selectedPlanTypeValue!.trim().isNotEmpty); + } + + String _toEditText(dynamic value) { + if (value == null) return ''; + final s = value.toString().trim(); + if (s.isEmpty || s.toLowerCase() == 'null') return ''; + return s; + } + + void _setEditControllersFromRow(Map row) { + _editVehicleTypeController.text = _toEditText(row['vehicle_type']); + _editFuelController.text = _toEditText(row['fuel']); + _editRtoController.text = _toEditText(row['rto']); + _editSegmentController.text = _toEditText(row['segment']); + _editBrokerController.text = _toEditText(row['broker_name']); + _editCompController.text = _toEditText(row['comp']); + _editTpController.text = _toEditText(row['tp']); + _editOdController.text = _toEditText(row['od']); + _editBrokerCompController.text = _toEditText(row['broker_comp']); + _editBrokerTpController.text = _toEditText(row['broker_tp']); + _editBrokerOdController.text = _toEditText(row['broker_od']); + _editPartnerCompController.text = _toEditText(row['partner_comp']); + _editPartnerTpController.text = _toEditText(row['partner_tp']); + _editPartnerOdController.text = _toEditText(row['partner_od']); + _editRemarksController.text = _toEditText(row['remarks']); + } + + void _clearEditControllers() { + _editVehicleTypeController.clear(); + _editFuelController.clear(); + _editRtoController.clear(); + _editSegmentController.clear(); + _editBrokerController.clear(); + _editCompController.clear(); + _editTpController.clear(); + _editOdController.clear(); + _editBrokerCompController.clear(); + _editBrokerTpController.clear(); + _editBrokerOdController.clear(); + _editPartnerCompController.clear(); + _editPartnerTpController.clear(); + _editPartnerOdController.clear(); + _editRemarksController.clear(); + } + + InputDecoration _editFieldDecoration(String label) { + return InputDecoration( + labelText: label, + labelStyle: GoogleFonts.poppins(fontSize: 12), + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ); + } + + Widget _modalField( + String label, + TextEditingController controller, { + int maxLines = 1, + }) { + return TextField( + controller: controller, + maxLines: maxLines, + style: GoogleFonts.inter(fontSize: 12), + decoration: _editFieldDecoration(label), + ); + } + + + @override + Widget build(BuildContext context) { + return MainLayout( + title: 'Grid Details', + body: SelectionArea( + child: Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTopBar(context), + const SizedBox(height: 10), + _buildFilterBar(context), + const SizedBox(height: 10), + if (!ResponsiveLayout.isMobile(context)) _buildDesktopHeader(), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildGridOrCards(context), + ), + SizedBox( + width: double.infinity, + child: Align( + alignment: Alignment.centerRight, + child: PaginationControls( + currentPage: _currentPage, + itemsPerPage: _itemsPerPage, + totalItems: _filteredGrid.length, + onPageChanged: (page) => setState(() => _currentPage = page), + onItemsPerPageChanged: (items) { + setState(() { + _itemsPerPage = items; + _currentPage = 1; + }); + }, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildTopBar(BuildContext context) { + final isAgent = + (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() == + 'agent'; + return Row( + children: [ + if (!isAgent) ...[ + Tooltip( + message: 'Back', + child: IconButton( + icon: const Icon(Icons.arrow_left_sharp, size: 25), + onPressed: () => context.go(AppRoutes.gridList), + splashRadius: 18, + padding: const EdgeInsets.all(4), + hoverColor: Colors.black12, + constraints: const BoxConstraints(), + ), + ), + const SizedBox(width: 5), + ], + Text( + 'Grid Details', + style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400), + ), + const Spacer(), + // Text( + // 'Retention Rate: $_retentionRate%', + // style: GoogleFonts.poppins( + // fontSize: 12, + // fontWeight: FontWeight.w500, + // color: const Color(0xFF2E7D6E), + // ), + // ), + ], + ); + } + + Widget _buildFilterBar(BuildContext context) { + final isMobile = ResponsiveLayout.isMobile(context); + + final planTypeItems = List>.from(_planTypes); + Map? selectedPlanType; + if (_selectedPlanTypeValue != null) { + for (final e in planTypeItems) { + if ((e['value'] as String?) == _selectedPlanTypeValue) { + selectedPlanType = e; + break; + } + } + } + + Widget dropdownInsurer() { + return SizedBox( + width: isMobile ? double.infinity : 200, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _insurers; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedInsurer, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? '', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedInsurer = val), + decoratorProps: DropDownDecoratorProps( + decoration: _dropdownDecoration('Insurer'), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget dropdownPlanType() { + return SizedBox( + width: isMobile ? double.infinity : 220, + height: 40, + child: DropdownSearch>( + items: (filter, infiniteScrollProps) { + final items = planTypeItems; + if (filter.isEmpty) return items; + return items + .where((e) => + (e['label']?.toString() ?? '') + .toLowerCase() + .contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: selectedPlanType, + itemAsString: (item) => item['label']?.toString() ?? '', + compareFn: (a, b) => (a['value'] == b['value']), + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem?['label']?.toString() ?? '', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() { + _selectedPlanTypeValue = val?['value'] as String?; + }), + decoratorProps: DropDownDecoratorProps( + decoration: _dropdownDecoration('Plan Type'), + ), + ), + ); + } + + Widget dropdownRto() { + return SizedBox( + width: isMobile ? double.infinity : 180, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _rtos; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedRto, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? '', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedRto = val), + decoratorProps: DropDownDecoratorProps( + decoration: _dropdownDecoration('RTO'), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget dropdownVehicleType() { + return SizedBox( + width: isMobile ? double.infinity : 200, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _vehicleTypes; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedVehicleType, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? '', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedVehicleType = val), + decoratorProps: DropDownDecoratorProps( + decoration: _dropdownDecoration('Vehicle Type'), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget dropdownSegment() { + return SizedBox( + width: isMobile ? double.infinity : 220, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _segments; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedSegment, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? '', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedSegment = val), + decoratorProps: DropDownDecoratorProps( + decoration: _dropdownDecoration('Segment'), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget searchBtn() => _filterIconButton( + icon: Icons.search, + message: 'Search', + onTap: () { + if (_showAddButton && !_hasAnyFilterSelection) { + ToastHelper.showInfoToast( + context, + 'Please select at least one filter to view/export data.', + ); + return; + } + _applyFilters(); + }, + ); + + Widget resetBtn() => _filterIconButton( + icon: Icons.refresh, + message: 'Reset', + onTap: () { + setState(() { + _selectedInsurer = null; + _selectedPlanTypeValue = null; + _selectedRto = null; + _selectedVehicleType = null; + _selectedSegment = null; + _searchController.clear(); + }); + _applyFilters(); + }, + ); + + return SizedBox( + width: double.infinity, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: isMobile + ? Column( + children: [ + dropdownInsurer(), + const SizedBox(height: 10), + dropdownPlanType(), + const SizedBox(height: 10), + dropdownVehicleType(), + const SizedBox(height: 10), + dropdownSegment(), + const SizedBox(height: 10), + dropdownRto(), + const SizedBox(height: 10), + _buildSearch(context, isMobile), + const SizedBox(height: 10), + searchBtn(), + const SizedBox(height: 10), + resetBtn(), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildExportButtonWidget(), + ], + ), + ], + ) + : Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + dropdownInsurer(), + const SizedBox(width: 12), + dropdownPlanType(), + const SizedBox(width: 12), + dropdownVehicleType(), + const SizedBox(width: 12), + dropdownSegment(), + const SizedBox(width: 12), + dropdownRto(), + const SizedBox(width: 12), + searchBtn(), + const SizedBox(width: 8), + resetBtn(), + ], + ), + ), + ), + const SizedBox(width: 20), + _buildSearch(context, isMobile), + const SizedBox(width: 12), + _buildExportButtonWidget(), + ], + ), + ), + ); + } + + Widget _buildSearch(BuildContext context, bool isMobile) { + return ThemedSearchField( + hintText: 'Search...', + controller: _searchController, + txtHeight: 40, + txtwidth: isMobile ? null : 240, + backgroundColor: Colors.white, + onChanged: (_) => _applyFilters(), + ); + } + + InputDecoration _dropdownDecoration(String label) { + return AppInputDecorations.dropdownDecoration(label: label).copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFE2E8F0)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFE2E8F0)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFF2E7D6E)), + ), + ); + } + + Widget _filterIconButton({ + required IconData icon, + required String message, + required VoidCallback onTap, + }) { + return Tooltip( + message: message, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Icon(icon, size: 18, color: const Color(0xFF334155)), + ), + ), + ); + } + + Widget _buildGridOrCards(BuildContext context) { + if (_paginatedGrid.isEmpty) { + return const Center(child: Text('No available data')); + } + + if (ResponsiveLayout.isMobile(context)) { + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + itemCount: _paginatedGrid.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) => _gridViewCard( + row: _paginatedGrid[index], + showCompColumn: _showCompColumnForAgent, + showTpColumn: _showTpColumnForAgent, + showOdColumn: _showOdColumnForAgent, + showPartnerCompValue: _showAddButton, + ), + ); + } + + return ListView.builder( + itemCount: _paginatedGrid.length, + itemBuilder: (context, index) { + return _buildTableRow(_paginatedGrid[index], rowIndex: index); + }, + ); + } + + Widget _buildDesktopHeader() { + return Container( + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + Expanded(flex: 1, child: Text('S.No', style: _headerStyle)), + Expanded(flex: 2, child: Text('Insurer', style: _headerStyle)), + Expanded(flex: 2, child: Text('Vehicle', style: _headerStyle)), + Expanded(flex: 3, child: Text('Segment', style: _headerStyle)), + Expanded(flex: 2, child: Text('RTO', style: _headerStyle)), + if (_showCompColumnForAgent) + Expanded(flex: 2, child: Text('Comp', style: _headerStyle)), + if (_showTpColumnForAgent) + Expanded(flex: 2, child: Text('TP', style: _headerStyle)), + if (_showOdColumnForAgent) + Expanded(flex: 2, child: Text('OD', style: _headerStyle)), + Expanded(flex: 3, child: Text('Remarks', style: _headerStyle)), + ], + ), + ); + } + + Widget _buildTableRow(Map row, {required int rowIndex}) { + final sno = (_currentPage - 1) * _itemsPerPage + rowIndex + 1; + + return Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), + ), + ), + child: Row( + children: [ + Expanded(flex: 1, child: Text('$sno', style: _dataBold)), + Expanded( + flex: 2, + child: Text(_nullableString(row['insurer']), style: _dataBold), + ), + Expanded( + flex: 2, + child: Text(_nullableString(row['vehicle_type']), style: _dataBold), + ), + Expanded( + flex: 3, + child: Text( + _nullableString(row['segment']), + style: _dataBold, + overflow: TextOverflow.ellipsis, + ), + ), + Expanded(flex: 2, child: Text(_nullableString(row['rto']), style: _dataBold)), + if (_showCompColumnForAgent) + Expanded( + flex: 2, + child: Text( + _showPartnerRetentionValues + ? _nullableString(row['partner_comp']) + : _nullableString(row['comp']), + style: _dataBold, + ), + ), + if (_showTpColumnForAgent) + Expanded(flex: 2, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${_nullableString(row['tp'])}', style: _dataBold), + if (_showPartnerRetentionValues) ...[ + const SizedBox(height: 2), + Text( + '${_nullableString(row['partner_tp'])}', + style: _dataBold, + ), + ], + ], + ), + ), + if (_showOdColumnForAgent) + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('${_nullableString(row['od'])}', style: _dataBold), + if (_showPartnerRetentionValues) ...[ + const SizedBox(height: 2), + Text( + '${_nullableString(row['partner_od'])}', + style: _dataBold, + ), + ], + ], + ), + ), + Expanded( + flex: 3, + child: Text( + _nullableString(row['remarks']), + style: _dataBold, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + static final _headerStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: const Color(0xFF1E293B), + ); + + static final _dataBold = GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w400, + color: const Color(0xFF000000), + ); + + Widget _toolbarIconButton({ + required IconData icon, + required String message, + required VoidCallback onTap, + }) { + return Tooltip( + message: message, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: const Color(0xFF2E7D6E), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 16, color: Colors.white), + ), + ), + ); + } + + Widget _buildExportButtonWidget() { + return InkWell( + onTap: _isLoading + ? () {} + : () async { + if (_showAddButton && !_hasAnyFilterSelection) { + if (mounted) { + ToastHelper.showInfoToast( + context, + 'Please select at least one filter to view/export data.', + ); + } + return; + } + try { + final queryParams = Uri.base.queryParameters; + final fileIdFromQuery = queryParams['file_id']?.trim(); + final role = _apiRoleFromAppRole(ref.read(userRoleProvider)); + final roleLc = (role ?? '').toLowerCase(); + final fileIdToSend = + (roleLc == 'manager' || roleLc == 'accounts') + ? fileIdFromQuery + : null; + await _apiService.downloadGridExcel( + role: role, + fileId: fileIdToSend, + insurer: _selectedInsurer, + vehicleType: _selectedVehicleType, + segment: _selectedSegment, + rto: _selectedRto, + planType: _selectedPlanTypeValue, + search: _searchController.text.trim(), + loggedId: _loggedId, + ); + } catch (e) { + if (mounted) { + ToastHelper.showErrorToast( + context, + e.toString().replaceFirst('Exception: ', ''), + ); + } + } + }, + child: Container( + width: 40, + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: const Color(0xFF2E7D6E), + ), + child: Tooltip( + message: 'Export', + child: Image.asset( + "assets/miscellaneous/export.png", + height: 13, + width: 13, + ), + ), + ), + ); + } +} + +class _gridViewCard extends StatelessWidget { + final Map row; + final bool showCompColumn; + final bool showTpColumn; + final bool showOdColumn; + final bool showPartnerCompValue; + + const _gridViewCard({ + required this.row, + required this.showCompColumn, + required this.showTpColumn, + required this.showOdColumn, + required this.showPartnerCompValue, + }); + + String _nullableString(dynamic v) { + if (v == null) return '-'; + final s = v.toString().trim(); + if (s.isEmpty || s.toLowerCase() == 'null') return '-'; + return s; + } + + @override + Widget build(BuildContext context) { + final id = row['id']?.toString() ?? '-'; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFEAEAEA)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + '${_nullableString(row['insurer'])} | ${_nullableString(row['vehicle_type'])}', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + 'ID: $id', + style: GoogleFonts.poppins( + fontSize: 10, + color: const Color(0xFF6E6E6E), + ), + ), + ], + ), + const SizedBox(height: 10), + Text('Segment: ${_nullableString(row['segment'])}', + style: GoogleFonts.inter(fontSize: 12)), + const SizedBox(height: 6), + Text('RTO: ${_nullableString(row['rto'])}', + style: GoogleFonts.inter(fontSize: 12)), + const SizedBox(height: 10), + Row( + children: [ + if (showCompColumn) + Expanded( + child: _upDownMetric( + title: 'Comp', + value: showPartnerCompValue ? row['partner_comp'] : row['comp'], + ), + ), + if (showCompColumn && (showTpColumn || showOdColumn)) + const SizedBox(width: 10), + if (showTpColumn) + Expanded(child: _metric('TP', row['tp'])), + if (showTpColumn && showOdColumn) + const SizedBox(width: 10), + if (showOdColumn) + Expanded(child: _metric('OD', row['od'])), + ], + ), + const SizedBox(height: 10), + Text('Remarks: ${_nullableString(row['remarks'])}', + style: GoogleFonts.inter(fontSize: 12)), + ], + ), + ); + } + + Widget _metric(String label, dynamic value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.poppins( + fontSize: 10, + color: const Color(0xFF64748B), + ), + ), + const SizedBox(height: 2), + Text( + _nullableString(value), + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Widget _upDownMetric({ + required String title, + required dynamic value, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: GoogleFonts.poppins( + fontSize: 10, + color: const Color(0xFF64748B), + ), + ), + const SizedBox(height: 2), + Text( + _nullableString(value), + style: GoogleFonts.poppins( + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} +/* import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:nhance_partner/core/services/api_service.dart'; +import 'package:nhance_partner/data/utils/Pagination.dart'; +import 'package:nhance_partner/data/utils/toastNotification.dart'; +import 'package:nhance_partner/presentation/layouts/main_layout.dart'; +import 'package:nhance_partner/presentation/layouts/responsive_layout.dart'; +import 'package:nhance_partner/presentation/providers/userRoleProvider.dart'; +import 'package:nhance_partner/presentation/screens/UserManagement/Agent/agentGridFile.dart'; +import 'package:nhance_partner/presentation/themes/indicators/export_btn.dart'; +import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart'; +import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart'; + +class gridViewScreen extends ConsumerStatefulWidget { + const gridViewScreen({super.key}); + + @override + ConsumerState createState() => _gridViewScreenState(); +} + +class _gridViewScreenState extends ConsumerState { + final ApiService _apiService = ApiService(); + + bool _isLoading = false; + + // API response + List> _grid = []; + List> _filteredGrid = []; + + List _insurers = []; + List> _planTypes = []; + List _rtos = []; + List _vehicleTypes = []; + List _segments = []; + + // Filters + String? _selectedInsurer; + String? _selectedRto; + String? _selectedVehicleType; + String? _selectedSegment; + String? _selectedPlanTypeValue; // comp/tp/od + + final TextEditingController _searchController = TextEditingController(); + + // Pagination + int _currentPage = 1; + int _itemsPerPage = 10; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _fetch(); + } + + String? _apiRoleFromAppRole(String? role) { + if (role == null) return null; + final r = role.trim(); + if (r.isEmpty) return null; + + switch (r.toLowerCase()) { + case 'manager': + return 'Manager'; + case 'handler': + return 'Handler'; + case 'staff': + return 'Staff'; + case 'agent': + return 'Agent'; + case 'accounts': + return 'Accounts'; + default: + return r; + } + } + + Future _fetch() async { + setState(() => _isLoading = true); + try { + final appRole = ref.read(userRoleProvider); + final apiRole = _apiRoleFromAppRole(appRole); + final queryParams = Uri.base.queryParameters; + final queryRole = queryParams['role']?.trim(); + final queryFileId = queryParams['file_id']?.trim(); + final effectiveRole = + (queryRole != null && queryRole.isNotEmpty) ? queryRole : apiRole; + String? effectiveFileId = + (queryFileId != null && queryFileId.isNotEmpty) ? queryFileId : null; + + if (effectiveFileId == null && + (effectiveRole?.toLowerCase() == 'manager' || + effectiveRole?.toLowerCase() == 'accounts')) { + final fileListRes = await _apiService.fetchGridFileList(); + if ((fileListRes['status'] ?? '').toString().toLowerCase() == + 'success') { + final files = List>.from( + fileListRes['data'] ?? const [], + ); + if (files.isNotEmpty) { + files.sort((a, b) { + final left = int.tryParse(a['id']?.toString() ?? '') ?? 0; + final right = int.tryParse(b['id']?.toString() ?? '') ?? 0; + return right.compareTo(left); + }); + effectiveFileId = files.first['id']?.toString(); + } + } + } + + final res = await _apiService.loadPayoutGrid( + role: effectiveRole, + fileId: effectiveFileId, + ); + if ((res['status'] ?? '').toString().toLowerCase() != 'success') { + ToastHelper.showErrorToast( + context, + res['data']?.toString() ?? 'Failed to load payout grid', + ); + return; + } + + final grid = res['grid']; + final insurers = res['insurers']; + final planTypes = res['plan_types']; + final rtos = res['rtos']; + final vehicleTypes = res['vehicle_types']; + final segments = res['segments']; + + _grid = (grid is List) + ? grid + .map>( + (e) => (e as Map).cast()) + .toList() + : >[]; + _filteredGrid = List>.from(_grid); + + _insurers = insurers is List ? insurers.map((e) => e.toString()).toList() : []; + _planTypes = planTypes is List + ? planTypes + .map>( + (e) => (e as Map).cast()) + .toList() + : []; + _rtos = rtos is List ? rtos.map((e) => e.toString()).toList() : []; + _vehicleTypes = + vehicleTypes is List ? vehicleTypes.map((e) => e.toString()).toList() : []; + _segments = + segments is List ? segments.map((e) => e.toString()).toList() : []; + + _applyFilters(); + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _setFilter(VoidCallback fn) { + fn(); + _applyFilters(); + } + + void _applyFilters() { + final q = _searchController.text.trim().toLowerCase(); + + final next = _grid.where((row) { + bool equalsOrAll(String? selected, dynamic raw) { + if (selected == null) return true; + return raw?.toString().toLowerCase() == selected.toLowerCase(); + } + + bool matchesPlanType() { + if (_selectedPlanTypeValue == null) return true; + final raw = row[_selectedPlanTypeValue]; + if (raw == null) return false; + final s = raw.toString().trim().toLowerCase(); + return s.isNotEmpty && s != 'null'; + } + + bool matchesSearch() { + if (q.isEmpty) return true; + final values = [ + row['id']?.toString() ?? '', + row['insurer']?.toString() ?? '', + row['vehicle_type']?.toString() ?? '', + row['segment']?.toString() ?? '', + row['rto']?.toString() ?? '', + row['remarks']?.toString() ?? '', + row['comp']?.toString() ?? '', + row['tp']?.toString() ?? '', + row['od']?.toString() ?? '', + ].map((e) => e.toLowerCase()); + + return values.any((v) => v.contains(q)); + } + + return equalsOrAll(_selectedInsurer, row['insurer']) && + equalsOrAll(_selectedVehicleType, row['vehicle_type']) && + equalsOrAll(_selectedSegment, row['segment']) && + equalsOrAll(_selectedRto, row['rto']) && + matchesPlanType() && + matchesSearch(); + }).toList(); + + setState(() { + _filteredGrid = next; + _currentPage = 1; + }); + } + + List> get _sortedGrid { + final next = [..._filteredGrid]; + next.sort((a, b) { + final ai = int.tryParse(a['id']?.toString() ?? '') ?? 0; + final bi = int.tryParse(b['id']?.toString() ?? '') ?? 0; + return bi.compareTo(ai); + }); + return next; + } + + List> get _paginatedGrid { + final sorted = _sortedGrid; + if (sorted.isEmpty) return []; + + final maxPage = (sorted.length / _itemsPerPage).ceil(); + final safePage = + _currentPage.clamp(1, maxPage == 0 ? 1 : maxPage) as int; + + final startIndex = (safePage - 1) * _itemsPerPage; + final endIndex = (startIndex + _itemsPerPage).clamp(0, sorted.length); + return sorted.sublist(startIndex, endIndex); + } + + String _nullableString(dynamic v) { + if (v == null) return '-'; + final s = v.toString().trim(); + if (s.isEmpty || s.toLowerCase() == 'null') return '-'; + return s; + } + + @override + Widget build(BuildContext context) { + return MainLayout( + title: 'Grid Details', + body: SelectionArea( + child: Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + _buildTopBar(context), + const SizedBox(height: 10), + _buildFilterBar(context), + const SizedBox(height: 10), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildGridOrCards(context), + ), + PaginationControls( + currentPage: _currentPage, + itemsPerPage: _itemsPerPage, + totalItems: _filteredGrid.length, + onPageChanged: (page) => setState(() => _currentPage = page), + onItemsPerPageChanged: (items) { + setState(() { + _itemsPerPage = items; + _currentPage = 1; + }); + }, + ), + ], + ), + ), + ), + ); + } + + Widget _buildTopBar(BuildContext context) { + final isAgent = + (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() == + 'agent'; + return Row( + children: [ + if (!isAgent) ...[ + Tooltip( + message: 'Back', + child: IconButton( + icon: const Icon(Icons.arrow_left_sharp, size: 25), + onPressed: () => context.go(AppRoutes.gridList), + splashRadius: 18, + padding: const EdgeInsets.all(4), + hoverColor: Colors.black12, + constraints: const BoxConstraints(), + ), + ), + const SizedBox(width: 5), + ], + Text( + 'Grid Details', + style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400), + ), + const Spacer(), + ExportBtn( + sheetName: 'Grid Details', + fileName: 'payout_grid', + data: _filteredGrid, + displayHeaders: const [ + 'ID', + 'Insurer', + 'Vehicle Type', + 'Segment', + 'RTO', + 'Comp', + 'TP', + 'OD', + 'Remarks', + ], + keys: const [ + 'id', + 'insurer', + 'vehicle_type', + 'segment', + 'rto', + 'comp', + 'tp', + 'od', + 'remarks', + ], + ), + ], + ); + } + + Widget _buildFilterBar(BuildContext context) { + final isMobile = ResponsiveLayout.isMobile(context); + final screenWidth = MediaQuery.of(context).size.width; + final isCompactFilter = !isMobile && screenWidth <= 1400; + final fieldHeight = isCompactFilter ? 36.0 : 40.0; + final fieldFontSize = isCompactFilter ? 11.0 : 12.0; + final fieldSpacing = isCompactFilter ? 8.0 : 12.0; + final filterHorizontalPadding = isCompactFilter ? 10.0 : 12.0; + final filterVerticalPadding = isCompactFilter ? 8.0 : 10.0; + + final planTypeItems = >[ + {'value': null, 'label': 'All'}, + ..._planTypes, + ]; + + Widget dropdownInsurer() { + return SizedBox( + width: isMobile ? double.infinity : 200, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _insurers; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedInsurer, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? 'All', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedInsurer = val), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'Insurer') + .copyWith(filled: true, fillColor: Colors.white, isDense: true), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget dropdownPlanType() { + return SizedBox( + width: isMobile ? double.infinity : 220, + height: 40, + child: DropdownSearch>( + items: (filter, infiniteScrollProps) { + final items = planTypeItems; + if (filter.isEmpty) return items; + return items + .where((e) => + (e['label']?.toString() ?? '') + .toLowerCase() + .contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: planTypeItems.firstWhere( + (e) => (e['value'] as String?) == _selectedPlanTypeValue, + orElse: () => planTypeItems.first, + ), + itemAsString: (item) => item['label']?.toString() ?? '', + compareFn: (a, b) => (a['value'] == b['value']), + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem?['label']?.toString() ?? 'All', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() { + _selectedPlanTypeValue = val?['value'] as String?; + }), + decoratorProps: DropDownDecoratorProps( + decoration: + AppInputDecorations.dropdownDecoration(label: 'Plan Type') + .copyWith(filled: true, fillColor: Colors.white, isDense: true), + ), + ), + ); + } + + Widget dropdownRto() { + return SizedBox( + width: isMobile ? double.infinity : 180, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _rtos; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedRto, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? 'All', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedRto = val), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'RTO') + .copyWith(filled: true, fillColor: Colors.white, isDense: true), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget dropdownVehicleType() { + return SizedBox( + width: isMobile ? double.infinity : 200, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _vehicleTypes; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedVehicleType, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? 'All', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedVehicleType = val), + decoratorProps: DropDownDecoratorProps( + decoration: + AppInputDecorations.dropdownDecoration(label: 'Vehicle Type') + .copyWith(filled: true, fillColor: Colors.white, isDense: true), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget dropdownSegment() { + return SizedBox( + width: isMobile ? double.infinity : 220, + height: 40, + child: DropdownSearch( + items: (filter, infiniteScrollProps) { + final items = _segments; + if (filter.isEmpty) return items; + return items + .where((e) => e.toLowerCase().contains(filter.toLowerCase())) + .toList(); + }, + selectedItem: _selectedSegment, + popupProps: const PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? 'All', + style: GoogleFonts.poppins(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, + ), + ), + onChanged: (val) => _setFilter(() => _selectedSegment = val), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'Segment') + .copyWith(filled: true, fillColor: Colors.white, isDense: true), + ), + compareFn: (a, b) => a == b, + ), + ); + } + + Widget resetBtn() { + return SizedBox( + height: 40, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2E7D6E), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + onPressed: () => setState(() { + _selectedInsurer = null; + _selectedPlanTypeValue = null; + _selectedRto = null; + _selectedVehicleType = null; + _selectedSegment = null; + _searchController.clear(); + _applyFilters(); + }), + icon: const Icon(Icons.refresh, size: 18), + label: Text( + 'Reset', + style: GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w500), + ), + ), + ); + } + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: isMobile + ? Column( + children: [ + _buildSearch(context, isMobile), + const SizedBox(height: 10), + dropdownInsurer(), + const SizedBox(height: 10), + dropdownPlanType(), + const SizedBox(height: 10), + dropdownVehicleType(), + const SizedBox(height: 10), + dropdownSegment(), + const SizedBox(height: 10), + dropdownRto(), + const SizedBox(height: 10), + resetBtn(), + ], + ) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildSearch(context, isMobile), + const SizedBox(width: 12), + dropdownInsurer(), + const SizedBox(width: 12), + dropdownPlanType(), + const SizedBox(width: 12), + dropdownVehicleType(), + const SizedBox(width: 12), + dropdownSegment(), + const SizedBox(width: 12), + dropdownRto(), + const SizedBox(width: 12), + resetBtn(), + ], + ), + ), + ); + } + + Widget _buildSearch(BuildContext context, bool isMobile) { + return ThemedSearchField( + hintText: 'Search...', + controller: _searchController, + txtHeight: 40, + txtwidth: isMobile ? null : 240, + backgroundColor: Colors.white, + onChanged: (_) => _applyFilters(), + ); + } + + Widget _buildGridOrCards(BuildContext context) { + if (_paginatedGrid.isEmpty) { + return const Center(child: Text('No available data')); + } + + if (ResponsiveLayout.isMobile(context)) { + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + itemCount: _paginatedGrid.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) => _gridViewCard(row: _paginatedGrid[index]), + ); + } + + return ListView.builder( + itemCount: _paginatedGrid.length + 1, + itemBuilder: (context, index) { + if (index == 0) return _buildTableHeader(); + final rowIndex = index - 1; + return _buildTableRow(_paginatedGrid[rowIndex], rowIndex: rowIndex); + }, + ); + } + + Widget _buildTableHeader() { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + child: Row( + children: [ + Expanded(flex: 1, child: Text('S.No', style: _headerStyle)), + Expanded(flex: 2, child: Text('Insurer', style: _headerStyle)), + Expanded(flex: 2, child: Text('Vehicle', style: _headerStyle)), + Expanded(flex: 3, child: Text('Segment', style: _headerStyle)), + Expanded(flex: 2, child: Text('RTO', style: _headerStyle)), + Expanded(flex: 2, child: Text('Comp', style: _headerStyle)), + Expanded(flex: 2, child: Text('TP', style: _headerStyle)), + Expanded(flex: 2, child: Text('OD', style: _headerStyle)), + Expanded(flex: 3, child: Text('Remarks', style: _headerStyle)), + ], + ), + ); + } + + Widget _buildTableRow(Map row, {required int rowIndex}) { + final sno = (_currentPage - 1) * _itemsPerPage + rowIndex + 1; + + return Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), + ), + ), + child: Row( + children: [ + Expanded(flex: 1, child: Text('$sno', style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['insurer']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['vehicle_type']), style: _dataBold)), + Expanded( + flex: 3, + child: Text( + _nullableString(row['segment']), + style: _dataBold, + overflow: TextOverflow.ellipsis, + ), + ), + Expanded(flex: 2, child: Text(_nullableString(row['rto']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['comp']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['tp']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['od']), style: _dataBold)), + Expanded( + flex: 3, + child: Text( + _nullableString(row['remarks']), + style: _dataBold, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + static final _headerStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: const Color(0xFF1E293B), + ); + + static final _dataBold = GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w400, + color: const Color(0xFF000000), + ); +} + +class _gridViewCard extends StatelessWidget { + final Map row; + + const _gridViewCard({required this.row}); + + String _nullableString(dynamic v) { + if (v == null) return '-'; + final s = v.toString().trim(); + if (s.isEmpty || s.toLowerCase() == 'null') return '-'; + return s; + } + + @override + Widget build(BuildContext context) { + final id = row['id']?.toString() ?? '-'; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFEAEAEA)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + '${_nullableString(row['insurer'])} | ${_nullableString(row['vehicle_type'])}', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + 'ID: $id', + style: GoogleFonts.poppins( + fontSize: 10, + color: const Color(0xFF6E6E6E), + ), + ), + ], + ), + const SizedBox(height: 10), + Text('Segment: ${_nullableString(row['segment'])}', + style: GoogleFonts.inter(fontSize: 12)), + const SizedBox(height: 6), + Text('RTO: ${_nullableString(row['rto'])}', + style: GoogleFonts.inter(fontSize: 12)), + const SizedBox(height: 10), + Row( + children: [ + Expanded(child: _metric('Comp', row['comp'])), + const SizedBox(width: 10), + Expanded(child: _metric('TP', row['tp'])), + const SizedBox(width: 10), + Expanded(child: _metric('OD', row['od'])), + ], + ), + const SizedBox(height: 10), + Text('Remarks: ${_nullableString(row['remarks'])}', + style: GoogleFonts.inter(fontSize: 12)), + ], + ), + ); + } + + Widget _metric(String label, dynamic value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: GoogleFonts.poppins( + fontSize: 10, + color: const Color(0xFF64748B), + )), + const SizedBox(height: 2), + Text( + _nullableString(value), + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} + +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:nhance_partner/core/services/api_service.dart'; +import 'package:nhance_partner/data/utils/toastNotification.dart'; +import 'package:nhance_partner/data/utils/Pagination.dart'; +import 'package:nhance_partner/presentation/layouts/main_layout.dart'; +import 'package:nhance_partner/presentation/layouts/responsive_layout.dart'; +import 'package:nhance_partner/presentation/providers/userRoleProvider.dart'; +import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart'; +import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart'; +import 'package:nhance_partner/presentation/themes/indicators/export_btn.dart'; +import 'package:nhance_partner/presentation/screens/UserManagement/Agent/agentGridFile.dart'; + +class gridViewScreen extends ConsumerStatefulWidget { + const gridViewScreen({super.key}); + + @override + ConsumerState createState() => _gridViewScreenState(); +} + +class _gridViewScreenState extends ConsumerState { + final ApiService _apiService = ApiService(); + + bool _isLoading = false; + + // API response + List> _grid = []; + List> _filteredGrid = []; + + List _insurers = []; + List> _planTypes = []; + List _rtos = []; + List _vehicleTypes = []; + List _segments = []; + + // Filters + String? _selectedInsurer; + String? _selectedRto; + String? _selectedVehicleType; + String? _selectedSegment; + String? _selectedPlanTypeValue; // comp/tp/od + + final TextEditingController _searchController = TextEditingController(); + + // Pagination + int _currentPage = 1; + int _itemsPerPage = 10; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _fetch(); + } + + String? _apiRoleFromAppRole(String? role) { + if (role == null) return null; + final r = role.trim(); + if (r.isEmpty) return null; + + // API expects capitalized role names (example: role=Manager) + switch (r.toLowerCase()) { + case 'manager': + return 'Manager'; + case 'handler': + return 'Handler'; + case 'staff': + return 'Staff'; + case 'agent': + return 'Agent'; + case 'accounts': + return 'Accounts'; + default: + // fallback: keep original capitalization + return r; + } + } + + Future _fetch() async { + setState(() => _isLoading = true); + try { + final appRole = ref.read(userRoleProvider); + final apiRole = _apiRoleFromAppRole(appRole); + final queryParams = Uri.base.queryParameters; + final queryRole = queryParams['role']?.trim(); + final queryFileId = queryParams['file_id']?.trim(); + final effectiveRole = + (queryRole != null && queryRole.isNotEmpty) ? queryRole : apiRole; + String? effectiveFileId = + (queryFileId != null && queryFileId.isNotEmpty) ? queryFileId : null; + + if (effectiveFileId == null && + (effectiveRole?.toLowerCase() == 'manager' || + effectiveRole?.toLowerCase() == 'accounts')) { + final fileListRes = await _apiService.fetchGridFileList(); + if ((fileListRes['status'] ?? '').toString().toLowerCase() == + 'success') { + final files = List>.from( + fileListRes['data'] ?? const [], + ); + if (files.isNotEmpty) { + files.sort((a, b) { + final left = int.tryParse(a['id']?.toString() ?? '') ?? 0; + final right = int.tryParse(b['id']?.toString() ?? '') ?? 0; + return right.compareTo(left); + }); + effectiveFileId = files.first['id']?.toString(); + } + } + } + + final res = await _apiService.loadPayoutGrid( + role: effectiveRole, + fileId: effectiveFileId, + ); + + if ((res['status'] ?? '').toString().toLowerCase() != 'success') { + ToastHelper.showErrorToast( + context, + res['data']?.toString() ?? 'Failed to load payout grid', + ); + return; + } + + final grid = res['grid']; + final insurers = res['insurers']; + final planTypes = res['plan_types']; + final rtos = res['rtos']; + final vehicleTypes = res['vehicle_types']; + final segments = res['segments']; + + _grid = (grid is List) + ? grid.map>((e) => (e as Map).cast()).toList() + : >[]; + _filteredGrid = List>.from(_grid); + + _insurers = insurers is List ? insurers.map((e) => e.toString()).toList() : []; + _planTypes = planTypes is List + ? planTypes + .map>((e) => (e as Map).cast()) + .toList() + : []; + _rtos = rtos is List ? rtos.map((e) => e.toString()).toList() : []; + _vehicleTypes = vehicleTypes is List ? vehicleTypes.map((e) => e.toString()).toList() : []; + _segments = segments is List ? segments.map((e) => e.toString()).toList() : []; + + // Ensure filters apply after fresh data load. + _applyFilters(); + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _applyFilters() { + final q = _searchController.text.trim().toLowerCase(); + + List> next = _grid.where((row) { + bool matchesInsurer() { + if (_selectedInsurer == null) return true; + return row['insurer']?.toString().toLowerCase() == _selectedInsurer!.toLowerCase(); + } + + bool matchesRto() { + if (_selectedRto == null) return true; + return row['rto']?.toString().toLowerCase() == _selectedRto!.toLowerCase(); + } + + bool matchesVehicleType() { + if (_selectedVehicleType == null) return true; + return row['vehicle_type']?.toString().toLowerCase() == _selectedVehicleType!.toLowerCase(); + } + + bool matchesSegment() { + if (_selectedSegment == null) return true; + return row['segment']?.toString().toLowerCase() == _selectedSegment!.toLowerCase(); + } + + bool matchesPlanType() { + if (_selectedPlanTypeValue == null) return true; + final v = _selectedPlanTypeValue!; + final raw = row[v]; + if (raw == null) return false; + final s = raw.toString().trim().toLowerCase(); + if (s.isEmpty || s == 'null') return false; + // Treat 0 as "not configured" only when backend returns 0. + return s != '0'; + } + + bool matchesSearch() { + if (q.isEmpty) return true; + + final id = row['id']?.toString().toLowerCase() ?? ''; + final insurer = row['insurer']?.toString().toLowerCase() ?? ''; + final vehicleType = row['vehicle_type']?.toString().toLowerCase() ?? ''; + final segment = row['segment']?.toString().toLowerCase() ?? ''; + final rto = row['rto']?.toString().toLowerCase() ?? ''; + final remarks = row['remarks']?.toString().toLowerCase() ?? ''; + final comp = row['comp']?.toString().toLowerCase() ?? ''; + final tp = row['tp']?.toString().toLowerCase() ?? ''; + final od = row['od']?.toString().toLowerCase() ?? ''; + + return [id, insurer, vehicleType, segment, rto, remarks, comp, tp, od].any((s) => s.contains(q)); + } + + return matchesInsurer() && + matchesRto() && + matchesVehicleType() && + matchesSegment() && + matchesPlanType() && + matchesSearch(); + }).toList(); + + setState(() { + _filteredGrid = next; + _currentPage = 1; + }); + } + + List> get _sortedGrid { + final next = [..._filteredGrid]; + next.sort((a, b) { + final ai = int.tryParse(a['id']?.toString() ?? '') ?? 0; + final bi = int.tryParse(b['id']?.toString() ?? '') ?? 0; + return bi.compareTo(ai); + }); + return next; + } + + List> get _paginatedGrid { + final sorted = _sortedGrid; + if (sorted.isEmpty) return []; + + final maxPage = (sorted.length / _itemsPerPage).ceil(); + final safePage = _currentPage.clamp(1, maxPage == 0 ? 1 : maxPage); + + final startIndex = (safePage - 1) * _itemsPerPage; + final endIndex = (startIndex + _itemsPerPage).clamp(0, sorted.length); + return sorted.sublist(startIndex, endIndex); + } + + void _setFilter(VoidCallback fn) { + fn(); + _applyFilters(); + } + + String _nullableString(dynamic v) { + if (v == null) return '-'; + final s = v.toString(); + if (s.trim().isEmpty || s.trim().toLowerCase() == 'null') return '-'; + return s; + } + + @override + Widget build(BuildContext context) { + return MainLayout( + title: 'Grid Details', + body: SelectionArea( + child: Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + _buildTopBar(context), + const SizedBox(height: 10), + _buildFilterBar(context), + const SizedBox(height: 10), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildGridOrCards(context), + ), + PaginationControls( + currentPage: _currentPage, + itemsPerPage: _itemsPerPage, + totalItems: _filteredGrid.length, + onPageChanged: (page) => setState(() => _currentPage = page), + onItemsPerPageChanged: (items) { + setState(() { + _itemsPerPage = items; + _currentPage = 1; + }); + }, + ), + ], + ), + ), + ), + ); + } + + Widget _buildTopBar(BuildContext context) { + final isMobile = ResponsiveLayout.isMobile(context); + final isAgent = + (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() == + 'agent'; + + return Row( + children: [ + if (!isAgent) ...[ + Tooltip( + message: 'Back', + child: IconButton( + icon: const Icon(Icons.arrow_left_sharp, size: 25), + onPressed: () => context.go(AppRoutes.gridList), + splashRadius: 18, + padding: const EdgeInsets.all(4), + hoverColor: Colors.black12, + constraints: const BoxConstraints(), + ), + ), + const SizedBox(width: 5), + ], + Text( + 'Grid Details', + style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400), + ), + const Spacer(), + if (!isMobile) + ExportBtn( + sheetName: 'Grid Details', + fileName: 'payout_grid', + data: _filteredGrid, + displayHeaders: const [ + 'ID', + 'Insurer', + 'Vehicle Type', + 'Segment', + 'RTO', + 'Comp', + 'TP', + 'OD', + 'Remarks', + ], + keys: const [ + 'id', + 'insurer', + 'vehicle_type', + 'segment', + 'rto', + 'comp', + 'tp', + 'od', + 'remarks', + ], + ), + ], + ); + } + + Widget _buildFilterBar(BuildContext context) { + final isMobile = ResponsiveLayout.isMobile(context); + + final planTypeItems = >[ + {'value': null, 'label': 'All'}, + ..._planTypes, + ]; + + Widget dropdownInsurer() { + return SizedBox( + width: isMobile ? double.infinity : (isCompactFilter ? 180 : 200), + height: fieldHeight, + child: DropdownSearch( + items: _insurers, + selectedItem: _selectedInsurer, + dropdownSearchTextInputDecoration: InputDecoration( + hintText: 'Search insurer...', + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + isDense: true, + ), + dropdownBuilder: (context, selectedItem) => Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + selectedItem ?? 'All', + style: GoogleFonts.poppins( + fontSize: fieldFontSize, + color: Colors.black, + ), + overflow: TextOverflow.ellipsis, + ), + ), + popupProps: const PopupProps.menu( + showSearchBox: true, + ), + onChanged: (val) => _setFilter(() => _selectedInsurer = val), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'Insurer') + .copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize), + ), + ), + ), + ); + } + + Widget dropdownPlanType() { + return SizedBox( + width: isMobile ? double.infinity : (isCompactFilter ? 200 : 220), + height: fieldHeight, + child: DropdownSearch>( + items: planTypeItems, + selectedItem: planTypeItems.firstWhere( + (e) => (e['value'] as String?) == _selectedPlanTypeValue, + orElse: () => planTypeItems.first, + ), + itemAsString: (v) => (v['label'] ?? '').toString(), + compareFn: (a, b) => (a['value'] == b['value']), + popupProps: const PopupProps.menu(showSearchBox: true), + onChanged: (val) => _setFilter(() => _selectedPlanTypeValue = val?['value'] as String?), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'Plan Type') + .copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize), + ), + ), + ), + ); + } + + Widget dropdownRto() { + return SizedBox( + width: isMobile ? double.infinity : (isCompactFilter ? 160 : 180), + height: fieldHeight, + child: DropdownSearch( + items: _rtos, + selectedItem: _selectedRto, + popupProps: const PopupProps.menu(showSearchBox: true), + dropdownSearchTextInputDecoration: InputDecoration( + hintText: 'Search RTO...', + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + isDense: true, + ), + onChanged: (val) => _setFilter(() => _selectedRto = val), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'RTO') + .copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize), + ), + ), + ), + ); + } + + Widget dropdownVehicleType() { + return SizedBox( + width: isMobile ? double.infinity : (isCompactFilter ? 180 : 200), + height: fieldHeight, + child: DropdownSearch( + items: _vehicleTypes, + selectedItem: _selectedVehicleType, + popupProps: const PopupProps.menu(showSearchBox: true), + dropdownSearchTextInputDecoration: InputDecoration( + hintText: 'Search vehicle type...', + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + isDense: true, + ), + onChanged: (val) => _setFilter(() => _selectedVehicleType = val), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'Vehicle Type') + .copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize), + ), + ), + ), + ); + } + + Widget dropdownSegment() { + return SizedBox( + width: isMobile ? double.infinity : (isCompactFilter ? 200 : 220), + height: fieldHeight, + child: DropdownSearch( + items: _segments, + selectedItem: _selectedSegment, + popupProps: const PopupProps.menu(showSearchBox: true), + dropdownSearchTextInputDecoration: InputDecoration( + hintText: 'Search segment...', + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + isDense: true, + ), + onChanged: (val) => _setFilter(() => _selectedSegment = val), + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration(label: 'Segment') + .copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize), + ), + ), + ), + ); + } + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + padding: EdgeInsets.symmetric( + horizontal: filterHorizontalPadding, + vertical: filterVerticalPadding, + ), + child: isMobile + ? Column( + children: [ + _buildSearch(context, isCompact: isCompactFilter), + const SizedBox(height: 10), + dropdownInsurer(), + const SizedBox(height: 10), + dropdownPlanType(), + const SizedBox(height: 10), + dropdownVehicleType(), + const SizedBox(height: 10), + dropdownSegment(), + const SizedBox(height: 10), + dropdownRto(), + const SizedBox(height: 10), + _buildResetButton(), + ], + ) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildSearch(context, isCompact: isCompactFilter), + SizedBox(width: fieldSpacing), + dropdownInsurer(), + SizedBox(width: fieldSpacing), + dropdownPlanType(), + SizedBox(width: fieldSpacing), + dropdownVehicleType(), + SizedBox(width: fieldSpacing), + dropdownSegment(), + SizedBox(width: fieldSpacing), + dropdownRto(), + SizedBox(width: fieldSpacing), + _buildResetButton(isCompact: isCompactFilter), + ], + ), + ), + ); + } + + Widget _buildSearch(BuildContext context, {bool isCompact = false}) { + return ThemedSearchField( + hintText: 'Search...', + controller: _searchController, + txtHeight: isCompact ? 36 : 40, + txtwidth: ResponsiveLayout.isMobile(context) + ? null + : (isCompact ? 210 : 240), + onChanged: (_) => _applyFilters(), + backgroundColor: Colors.white, + ); + } + + Widget _buildResetButton({bool isCompact = false}) { + return SizedBox( + height: isCompact ? 36 : 40, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2E7D6E), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + onPressed: () { + _setFilter(() { + _selectedInsurer = null; + _selectedPlanTypeValue = null; + _selectedRto = null; + _selectedVehicleType = null; + _selectedSegment = null; + _searchController.clear(); + }); + }, + icon: Icon(Icons.refresh, size: isCompact ? 16 : 18), + label: Text( + 'Reset', + style: GoogleFonts.poppins( + fontSize: isCompact ? 11 : 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } + + Widget _buildGridOrCards(BuildContext context) { + if (_paginatedGrid.isEmpty) { + return const Center(child: Text('No available data')); + } + + if (ResponsiveLayout.isMobile(context)) { + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + itemCount: _paginatedGrid.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final row = _paginatedGrid[index]; + return _gridViewCard(row: row); + }, + ); + } + + return ListView.builder( + itemCount: _paginatedGrid.length + 1, // + header + itemBuilder: (context, index) { + if (index == 0) return _buildTableHeader(); + final rowIndex = index - 1; + final row = _paginatedGrid[rowIndex]; + return _buildTableRow(row, rowIndex: rowIndex); + }, + ); + } + + Widget _buildTableHeader() { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), + ), + ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + child: Row( + children: [ + Expanded(flex: 1, child: Text('S.No', style: _headerStyle)), + Expanded(flex: 2, child: Text('Insurer', style: _headerStyle)), + Expanded(flex: 2, child: Text('Vehicle', style: _headerStyle)), + Expanded(flex: 3, child: Text('Segment', style: _headerStyle)), + Expanded(flex: 2, child: Text('RTO', style: _headerStyle)), + Expanded(flex: 2, child: Text('Comp', style: _headerStyle)), + Expanded(flex: 2, child: Text('TP', style: _headerStyle)), + Expanded(flex: 2, child: Text('OD', style: _headerStyle)), + Expanded(flex: 3, child: Text('Remarks', style: _headerStyle)), + ], + ), + ); + } + + Widget _buildTableRow( + Map row, { + required int rowIndex, + }) { + final sno = (_currentPage - 1) * _itemsPerPage + rowIndex + 1; + + return Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), + ), + ), + child: Row( + children: [ + Expanded(flex: 1, child: Text('$sno', style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['insurer']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['vehicle_type']), style: _dataBold)), + Expanded(flex: 3, child: Text(_nullableString(row['segment']), style: _dataBold, overflow: TextOverflow.ellipsis)), + Expanded(flex: 2, child: Text(_nullableString(row['rto']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['comp']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['tp']), style: _dataBold)), + Expanded(flex: 2, child: Text(_nullableString(row['od']), style: _dataBold)), + Expanded(flex: 3, child: Text(_nullableString(row['remarks']), style: _dataBold, overflow: TextOverflow.ellipsis)), + ], + ), + ); + } + + static final _headerStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: const Color(0xFF1E293B), + ); + + static final _dataBold = GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w400, + color: const Color(0xFF000000), + ); +} + +class _gridViewCard extends StatelessWidget { + final Map row; + + const _gridViewCard({required this.row}); + + String _nullableString(dynamic v) { + if (v == null) return '-'; + final s = v.toString(); + if (s.trim().isEmpty || s.trim().toLowerCase() == 'null') return '-'; + return s; + } + + @override + Widget build(BuildContext context) { + final id = row['id']?.toString() ?? '-'; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFEAEAEA)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + '${_nullableString(row['insurer'])} | ${_nullableString(row['vehicle_type'])}', + style: GoogleFonts.poppins(fontSize: 13, fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + 'ID: $id', + style: GoogleFonts.poppins(fontSize: 10, color: const Color(0xFF6E6E6E)), + ), + ], + ), + const SizedBox(height: 10), + Text('Segment: ${_nullableString(row['segment'])}', style: GoogleFonts.inter(fontSize: 12)), + const SizedBox(height: 6), + Text('RTO: ${_nullableString(row['rto'])}', style: GoogleFonts.inter(fontSize: 12)), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: _metric('Comp', row['comp']), + ), + const SizedBox(width: 10), + Expanded( + child: _metric('TP', row['tp']), + ), + const SizedBox(width: 10), + Expanded( + child: _metric('OD', row['od']), + ), + ], + ), + const SizedBox(height: 10), + Text('Remarks: ${_nullableString(row['remarks'])}', style: GoogleFonts.inter(fontSize: 12)), + ], + ), + ); + } + + Widget _metric(String label, dynamic value) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: GoogleFonts.poppins(fontSize: 10, color: const Color(0xFF64748B))), + const SizedBox(height: 2), + Text(_nullableString(value), style: GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w600)), + ], + ), + ); + } +} + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import 'package:nhance_partner/core/services/api_service.dart'; +import 'package:nhance_partner/core/config/env.dart'; +import 'package:nhance_partner/data/utils/toastNotification.dart'; +import 'package:nhance_partner/data/services/auth_service.dart'; +import 'package:nhance_partner/presentation/layouts/main_layout.dart'; +import 'package:nhance_partner/presentation/providers/manager_provider.dart'; +import 'package:nhance_partner/presentation/providers/userRoleProvider.dart'; +import 'package:nhance_partner/presentation/themes/indicators/date_field_theme.dart'; +import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart'; +import 'package:nhance_partner/presentation/screens/UserManagement/Agent/agentGridFile.dart'; + +class gridViewScreen extends ConsumerStatefulWidget { + const gridViewScreen({super.key}); + @override + ConsumerState createState() => gridViewScreenState(); +} + +class gridViewScreenState extends ConsumerState { + int currentPage = 1; + int itemsPerPage = 10; + late ApiService apiService; + + // List> dataVal = []; + List> getStaffData = []; + List> originalData = []; + List> filteredData = []; + bool isLoading = false; + dynamic userId; + dynamic roleId; + dynamic managerId; + + // bool isDrawerOpen = false; + Map? selectedRow; + List> endorsementNumbers = []; + + List> getEndrosmentType = []; + List> filteredEndrosmentData = []; + String? hoveredRowId; + + String? selectedFileNames; + PlatformFile? docUploadedFile; + String? lastPickedFile; + + + @override + void initState() { + super.initState(); + + apiService = ApiService(); + + Future.microtask(() async { + final id = ref.read(managerIdProvider); + managerId = ref.read(managerIdProvider); + roleId = ref.read(userRoleProvider); + userId = ref.read(userIdProvider); + print("F46 => r : $roleId | mId: $id | uId: $userId "); + /// ⭐ DEFAULT FILTER FOR ACCOUNTS + if (roleId == 'Accounts') { + selectedVerificationVal = 0; // 0 = To Verify + } else { + selectedStatusVal = 'Open'; + } + + if (userId != null) { + await getStaffList(managerId, userId, roleId); + } + getEnroementType(); + getInsurerDetails(); + }); + } + + Future getStaffList(int managerId, userId, role) async { + setState(() { + isLoading = true; + }); + + print('fetchEndorsementList first $selectedVerificationVal'); + + try { + final response = await apiService.fetchEndorsementList( + managerId, + userId, + role, + + fromDate: startController.text.isNotEmpty + ? DateFormat('yyyy-MM-dd') + .format(DateFormat('dd-MM-yyyy') + .parse(startController.text)) + : null, + + toDate: endController.text.isNotEmpty + ? DateFormat('yyyy-MM-dd') + .format(DateFormat('dd-MM-yyyy') + .parse(endController.text)) + : null, + + endorsementType: + selectedEndorsementTypeVal?.toString(), + + insurerId: + selectedInsurerVal?.toString(), + + status: selectedStatusVal == 'All' + ? '' + : selectedStatusVal == 'Open' + ? 'Open' + : selectedStatusVal == 'Closed' + ? 'Closed' + : null, + + + verification: selectedVerificationVal == 2 + ? '' + : selectedVerificationVal == 0 + ? 'To Verify' + : selectedVerificationVal == 1 + ? 'Verified' + : null, + // selectedVerificationVal?.toString(), + ); + print('fetchEndorsementList'); + + if (response['status'] == 'success') { + final data = response['data']; + + setState(() { + if (data is List) { + getStaffData = + List>.from(data); + } else if (data is Map) { + getStaffData = [ + Map.from(data) + ]; + } else { + getStaffData = []; + } + + originalData = getStaffData; + filteredData = List.from(originalData); + }); + } else { + getStaffData = []; + originalData = []; + } + } catch (e) { + print('Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + List get _paginatedData { + // Sort descending by id first + final sortedData = [...filteredData] + ..sort((a, b) => int.parse(b['id']) - int.parse(a['id'])); + + if (sortedData.isEmpty) return []; + + // Ensure currentPage is valid + final maxPage = (sortedData.length / itemsPerPage).ceil(); + final safePage = currentPage.clamp(1, maxPage); + + final startIndex = (safePage - 1) * itemsPerPage; + final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length); + + return sortedData.sublist(startIndex, endIndex); + } + + void refresh() { + getStaffList(managerId, userId, roleId); + } + + void filterData(String query) { + final lowerQuery = query.toLowerCase(); + + setState(() { + filteredData = getStaffData.where((item) { + + /// 🔹 Search all raw values + final matchesRawValues = item.values.any( + (value) => + value != null && + value.toString().toLowerCase().contains(lowerQuery), + ); + + /// 🔹 Add computed verification text + final verificationText = + item['is_data_accuracy_checked'] == '0' + ? 'to verify' + : 'verified'; + + final matchesVerification = + verificationText.contains(lowerQuery); + + return matchesRawValues || matchesVerification; + + }).toList(); + }); + } + + void _confirmDelete(BuildContext context, String id) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.red.shade600, size: 22), + const SizedBox(width: 8), + Text( + 'Delete Endorsement', + style: GoogleFonts.poppins(fontSize: 15, fontWeight: FontWeight.w600), + ), + ], + ), + content: Text( + 'Are you sure you want to delete this endorsement?\nThis action cannot be undone.', + style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade700), + ), + actions: [ + // CANCEL + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text( + 'Cancel', + style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade600), + ), + ), + + // CONFIRM DELETE + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + onPressed: () async { + Navigator.pop(ctx); + await _deleteEndorsement(id); + }, + child: Text( + 'Delete', + style: GoogleFonts.poppins(fontSize: 13, color: Colors.white), + ), + ), + ], + ), + ); + } + + Future _deleteEndorsement(String id) async { + try { + setState(() => isLoading = true); + + final response = await apiService.deleteEndorsement(id); + + if (response['status'] == 'success' || response['code'] == 200) { + refresh(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Endorsement deleted successfully.', + style: GoogleFonts.poppins(fontSize: 13), + ), + backgroundColor: Colors.green.shade600, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + duration: const Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + response['message'] ?? 'Failed to delete endorsement.', + style: GoogleFonts.poppins(fontSize: 13), + ), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + duration: const Duration(seconds: 2), + ), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e', style: GoogleFonts.poppins(fontSize: 13)), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + } finally { + setState(() => isLoading = false); + } + } + + final TextEditingController rightSearchController = TextEditingController(); + final TextEditingController startController = TextEditingController(); + final TextEditingController endController = TextEditingController(); + + dynamic selectedStatusVal; + dynamic selectedVerificationVal; + dynamic selectedInsurerVal; + dynamic selectedInsurer; + dynamic selectedEndorsementTypeVal; + + List> getInsurerDetailsData = []; + List> filteredInsurerData = []; + bool isLoadingA = false; + + String _formatDate(String rawDate) { + try { + final dateTime = DateTime.parse(rawDate); + return DateFormat('dd-MM-yyyy HH:mm').format(dateTime); // 24-hour format + } catch (e) { + return rawDate; // fallback if parsing fails + } + } + + String _truncateText(String? text, int maxLength) { + if (text == null || text.length <= maxLength) { + return text ?? '-'; + } + return '${text.substring(0, maxLength)}...'; + } + + List _buildPopupMenuActions(BuildContext context, Map item) { + final String Id = item['id']?.toString() ?? ''; + final String? pdfPath = item["endorsement_completion_file"]; + final String? fileName = (pdfPath != null && pdfPath.isNotEmpty) + ? pdfPath.split('/').last + : null; + + return [ + // --- EDIT BUTTON --- + InkWell( + onTap: () { + Navigator.pop(context); // Close the menu first + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext dialogContext) { + return UpdateEndorsementDialog( + item: item, + userId: userId, // Ensure these variables are accessible + managerId: managerId, + onSubmit: (val) => refresh(), + ); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + child: Row( + children: const [ + Icon(Icons.edit_sharp, color: Color(0xFF319718), size: 18), + SizedBox(width: 12), + Text('Edit', style: TextStyle(fontSize: 14)), + ], + ), + ), + ), + + const Divider(height: 1, thickness: 0.5), // Subtle separator + + // --- DOWNLOAD BUTTON --- + InkWell( + onTap: () { + Navigator.pop(context); // Close the menu first + apiService.downloadFile( + apiUrl: 'endorsement/downloadEndorsementCompletionFile?id=$Id', + apiId: Id, + localFile: null, + fileName: fileName, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + child: Row( + children: const [ + Icon(Icons.download_rounded, color: Colors.blue, size: 18), + SizedBox(width: 12), + Text('Download', style: TextStyle(fontSize: 14)), + ], + ), + ), + ), + + // --- DELETE BUTTON --- + if (roleId == 'Accounts') ...[ + const Divider(height: 1, thickness: 0.5), + InkWell( + onTap: () { + Navigator.pop(context); + _confirmDelete(context, Id); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + child: Row( + children: const [ + Icon(Icons.delete_outline, color: Colors.red, size: 18), + SizedBox(width: 12), + Text('Delete', style: TextStyle(fontSize: 14, color: Colors.red)), + ], + ), + ), + ), + ], + ]; + } + + Future fetchEndorsementExcelReport() async { + final String searchValue = rightSearchController.text; + final response = await apiService.generateEndorsementExcel(managerId,searchValue); + // Map your controllers and IDs to the API parameters + // final String fromDate = controllers['startDate']!.text; // "11-01-2025" + // final String toDate = controllers['endDate']!.text; // "20-01-2025" + // print("fetchPolicyExcelReport function called here."); + // + // try { + // // Assuming you have a generic _makeGetRequest method + // final response = await apiService.fetchPolicyExcel(managerId,fromDate,toDate); + // + // if (response != null && response['status'] == 200) { + // setState(() { + // // Store the list of data in your variable + // excelResponseData = response['data']; + // }); + // print("Data fetched. Ready to export."); + // } + // } catch (e) { + // debugPrint("Export Error: $e"); + // print("Failed to fetch report data."); + // } + } + + Future getEnroementType() async { + print('Insurers called'); + setState(() { + isLoading = true; + }); + + try { + final response = await apiService.fetchMasterDropDown('Endorsement'); + + if (response['status'] == 200) { + print('getEnroementType - ${response['data']}'); + setState(() { + getEndrosmentType = List>.from(response['data']); + print('API Data - $getEndrosmentType'); + + filteredEndrosmentData = List.from(getEndrosmentType); + // print('originalData - $filteredEndrosmentData'); + }); + } else { + getEndrosmentType = []; + filteredEndrosmentData = []; + } + } catch (e) { + print('Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + Future getInsurerDetails() async { + setState(() => isLoadingA = true); + try { + final response = await apiService.fetchMasterDropDown('Insurers'); + + print("***999999999999999999*********** $response"); + + if (response['status'] == 200) { + print('getInsurerDetails - ${response['data']}'); + + setState(() { + getInsurerDetailsData = + List>.from(response['data']); + + filteredInsurerData = + List>.from(getInsurerDetailsData); + + print("Filtered Insurer Data: $filteredInsurerData"); + }); + } else { + setState(() { + getInsurerDetailsData = []; + filteredInsurerData = []; + }); + } + } catch (e) { + print('Error: $e'); + } finally { + setState(() => isLoadingA = false); + } + } + + Future loadEndorsementNumbers(String id) async { + // final response = await apiService.fetchEndorsementNumbers(id); + // + // if (response['status'] == 'success') { + // setState(() { + // endorsementNumbers = + // List>.from(response['data']); + // }); + // } + } + + @override + Widget build(BuildContext context) { + return MainLayout( + title: "Endorsement", + body: SelectionArea( + child: Container( + // color: Colors.yellow.shade50, + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Container( + // height: 30, + // width: MediaQuery.of(context).size.width, + // child: GestureDetector( + // onTap: () { + // context.go(AppRoutes.dashboard); + // }, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Tooltip( + // message: 'Back', + // child: IconButton( + // icon: const Icon( + // Icons.arrow_left_sharp, + // size: 25, + // color: Color(0xFF425B5B), + // ), + // onPressed: () { + // context.go(AppRoutes.dashboard); + // }, + // splashRadius: 18, + // hoverColor: Colors.black12, + // padding: const EdgeInsets.all(4), + // constraints: const BoxConstraints(), + // ), + // ), + // const SizedBox(width: 5), // spacing between icon and text + // Text( + // "Endorsement", + // style: GoogleFonts.poppins( + // fontSize: 14, + // fontWeight: FontWeight.w400, + // ), + // ), + // ], + // ), + // ), + // ), + // + // SizedBox(height: 5), + Expanded( + child: Container( + // color: Colors.green, + // color: Colors.green.shade50, + width: MediaQuery.of(context).size.width, + + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + Container( + // height: 40, + // color: Colors.pink, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Endorsement", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + SizedBox(height: 10), + Container( + width: double.infinity, + child: Row( + children: [ + Expanded( + child: buildFilterBar(context), + ), + ], + ), + ), + SizedBox(height: 10), + if (!ResponsiveLayout.isMobile(context)) + Container( + decoration: BoxDecoration( + color: Color(0xFFF1F5F9), + // color: Color(0xFFEDF6F5), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Row( + children: [ + Expanded( + flex: 1, + child: Text('ID ', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Vechile TYpe ', style: _headerStyle), + ), + Expanded( + flex: 3, + child: Text('Fuel ', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Insurer', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('RTO', style: _headerStyle), + ), + Expanded( + flex: 3, + child: Text('Broker', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Segment', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text( + 'Comp', + style: _headerStyle, + ), + ), + Expanded( + flex: 2, + child: Text( + 'TP', + style: _headerStyle, + ), + ), + Expanded( + flex: 2, + child: Text( + 'OD', + style: _headerStyle, + ), + ), + Expanded( + flex: 2, + child: Text( + 'Remarks', + style: _headerStyle, + ), + ), + Expanded( + flex: 2, + child: Text( + 'Partner Comp', + style: _headerStyle, + ), + ), + Expanded( + flex: 2, + child: Text( + 'Partner TD', + style: _headerStyle, + ), + ), + // if(roleId == 'Accounts') + Expanded( + flex: 2, + child: Text( + 'Partner OD', + style: _headerStyle, + ), + ), + Expanded( + flex: 2, + child: Text('Action', style: _headerStyle), + ), + ], + ), + ), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // MAIN TABLE + Expanded( + flex: 3, + child: Container( + // color: Colors.white, + child: _buildDataTable(context), + ), + ), + + // DRAWER (only if open) + // if (isDrawerOpen) + // Expanded( + // flex: 1, + // child: _buildDrawer(), + // ), + ], + ), + ), + ], + ), + ), + ), + + Container( + // height: 20, + width: MediaQuery.of(context).size.width, + // color: Colors.green.shade50, + child: PaginationControls( + currentPage: currentPage, + itemsPerPage: itemsPerPage, + // totalItems: dataVal.length, + totalItems: filteredData.length, + // activeColor: layoutColor, // your theme color + onPageChanged: (page) { + setState(() { + currentPage = page; + }); + }, + onItemsPerPageChanged: (items) { + setState(() { + itemsPerPage = items; + currentPage = 1; + }); + }, + ), + ), + ], + ), + ), ), + ); + } + + Widget buildFilterBar(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + child: Row( + children: [ + + /// 🔹 LEFT SIDE (SCROLLABLE FILTERS) + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + buildRTOSearch(context), + const SizedBox(width: 12), + + buildVechileTypeSearch(context), + const SizedBox(width: 12), + + buildInsurerSearch(context), + const SizedBox(width: 12), + + buildPalntypeSearch(context), + const SizedBox(width: 12), + + buildSegmentSearch(context), + const SizedBox(width: 12), + + _iconButton(Icons.search, () { + refresh(); + }), + const SizedBox(width: 8), + + _iconButton(Icons.refresh, () { + selectedRTOVal = null; + selectedInsurerVal = null; + selectedVechileTypeVal = null; + selectedPlanTypeVal = null; + selectedSegmentVal = null; + rightSearchController.clear(); + refresh(); + }), + ], + ), + ), + ), + + /// 🔹 RIGHT SIDE (FIXED) + const SizedBox(width: 20), + + _rightSearchField(), + const SizedBox(width: 12), + _exportButton(), + ], + ), + ), + ); + } + + Widget _buildDataTable(BuildContext context) { + if (filteredData.isEmpty) { + return const SizedBox( + height: 50, + child: Center(child: Text('No available data')), + ); + } + + final sortedData = [..._paginatedData]; + return ListView.builder( + itemCount: ResponsiveLayout.isMobile(context) + ? sortedData + .length // only cards for mobile + : sortedData.length + 1, // +1 for header in desktop + itemBuilder: (context, index) { + if (!ResponsiveLayout.isMobile(context) && index == 0) { + return _buildHeader(); + } + + final startIndex = (currentPage - 1) * itemsPerPage; + final item = + sortedData[index - (ResponsiveLayout.isMobile(context) ? 0 : 1)]; + final sno = startIndex + index; + + return !ResponsiveLayout.isMobile(context) + ? _buildDataRow(item, sno) + : _buildDataCard(item, sno); + }, + ); + } + + Widget _buildHeader() { + return SizedBox.shrink(); + } + + Widget _buildDataRow(Map item, sno) { + final String id = item['id']?.toString() ?? ''; + + + + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () async { + setState(() { + selectedRow = item; + // isDrawerOpen = true; + }); + + await loadEndorsementNumbers(item['id']); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 16), + decoration: BoxDecoration( + // color: bgColor, + border: const Border( + bottom: BorderSide( + color: Color(0xFFEAEAEA), + width: 1, + ), + ), + ), + child: Row( + children: [ + + Expanded(flex: 1, child: Text('$sno', style: _dataBold)), + + Expanded( + flex: 2, + child: Text( + item['vehicle_type'] != null + ? item['vehicle_type'].toString() + : '-', + style: _dataBold, + ), + ), + + Expanded( + flex: 3, + child: Text( + item['fuel'] != null + ? item['feul'].toString() + : '-', + style: _dataBold, + ), + ), + + Expanded( + flex: 2, + child: Text(item['insurer'] ?? '-', style: _dataBold)), + + Expanded( + flex: 2, + child: Text(item['rto'] ?? '-', style: _dataBold)), + + Expanded( + flex: 3, + child: + Text(item['broker_name'] ?? '-', style: _dataBold)), +Expanded( + flex: 2, + child: Text(item['swgment'] ?? '-', style: _dataBold)), + + Expanded( + flex: 2, + child: Text(item['comp'] ?? '-', style: _dataBold)), + Expanded( + flex: 2, + child: Text(item['tp'] ?? '-', style: _dataBold)), + Expanded( + flex: 2, + child: Text(item['od'] ?? '-', style: _dataBold)), + Expanded( + flex: 2, + child: Text(item['remarks'] ?? '-', style: _dataBold)), + Expanded( + flex: 2, + child: Text(item['partner_comp'] ?? '-', style: _dataBold)), + Expanded( + flex: 2, + child: Text(item['partner_tp'] ?? '-', style: _dataBold)), + Expanded( + flex: 2, + child: Text(item['partner_od'] ?? '-', style: _dataBold)), + Expanded( + flex: 2, + child: item['is_active'] == "1" + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + // --- EDIT --- + _actionIconButton( + context: context, + tooltip: 'Edit', + iconColor: const Color(0xFF319718), + onTap: () async { + _openEditModal(item); + if (result == true) refresh(); + }, + customIcon: Image.asset( + "assets/miscellaneous/Edit.png", + height: 15, + width: 15, + ), + ), + ], + ) + : const Text('-'), _buildListWidget + ), + ], + ), + ), + ), + ); + } + + + + Widget _buildDataCard(Map item, int sno) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF6FEFD), + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: const Color(0xffD9EBE8)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + + + + + + + + + Text(item['vehicle_type'] ?? '-', style: _headerStyle), + PopupMenuButton( + color: Colors.white, + padding: EdgeInsets.zero, + offset: Offset(0, 30), + icon: Icon(Icons.more_vert, color: Color(0xFF475569), size: 14), + itemBuilder: (context) => [ + CustomPopupMenuEntry( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: _buildPopupMenuActions(context, item), + ), + ), + ), + ], + ), + ], + ), + const Divider(color: Color(0xffD9EBE8), thickness: 0.8), + + // Policy From + Status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Text("Fuel", style: _cardheaderStyle), + Text( + item['fuel'] ?? '-', + style: _cardBodyStyle, + ), + ], + ), + ), + SizedBox(width: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //remarks + Text("remarks", style: _cardheaderStyle), + Text( + item['remarks'] ?? '-', + style: _cardBodyStyle, + maxLines: 3, + softWrap: true, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + + mainAxisAlignment: MainAxisAlignment.start, + children: [ + //company + Text("Insurer", style: _cardheaderStyle), + Text( + item['insurer'] ?? '-', + style: _cardBodyStyle, + maxLines: 3, + softWrap: true, + ), + ], + ), + ), + const SizedBox(height: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("RTO", style: _cardheaderStyle), + Text( + item['rto'] ?? '-', + style: _cardBodyStyle, + ), + ], + ), + const SizedBox(height: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + + Text("broker", style: _cardheaderStyle), + Text( + item['broker_name'] ?? '-', + style: _cardBodyStyle, + maxLines: 2, + softWrap: true, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 15), + + // Created On + Remarks + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Text("comp", style: _cardheaderStyle), + Text( + item['partner_comp'] '&' item['comp'] ?? '-', + style: _cardBodyStyle, + ), + ], + ), + ), + SizedBox(width: 5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Text("TP", style: _cardheaderStyle), + Text( + item['partner_tp'] '&' item['tp'] ?? '-', + style: _cardBodyStyle, + ), + ], + ), + ), + const SizedBox(height: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("OD", style: _cardheaderStyle), + Text( + item['partner_od'] '&' item['OD'] ?? '-', + style: _cardBodyStyle, + ), + ], + ), + ], + ), + ], + ), + ); + } + + Widget _buildDrawer() { + return Container( + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: const Color(0xFFEAEAEA)), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + + // HEADER + Container( + padding: const EdgeInsets.all(12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFEAEAEA)), + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + "${selectedRow?['']} - Grid", + style: _headerStyle, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(() { + // isDrawerOpen = false; + selectedRow = null; + }); + }, + ), + ], + ), + ), + + // TABLE + Expanded( + child: endorsementNumbers.isEmpty + ? const Center(child: Text("No ")) + : ListView.builder( + itemCount: endorsementNumbers.length, + itemBuilder: (context, index) { + final item = endorsementNumbers[index]; + return Container( + padding: const EdgeInsets.all(12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Color(0xFFEAEAEA), + ), + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + item['vechile_type'] ?? '-', + style: _dataBold, + ), + ), + IconButton( + icon: const Icon(Icons.edit, size: 16), + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.download, size: 16), + onPressed: () {}, + ), + ], + ), + ); + }, + ), + ), + + // PAGINATION + PaginationControls( + currentPage: 1, + itemsPerPage: 10, + totalItems: endorsementNumbers.length, + onPageChanged: (page) {}, + onItemsPerPageChanged: (items) {}, + ), + ], + ), + ); + } + + Widget buildStartDate(BuildContext context) { + return SizedBox( + height: 36, + width: ResponsiveLayout.isMobile(context) + ? null + : MediaQuery.of(context).size.width * 0.10, + child: ThemedDateField( + hintText: "Start Date", + controller: startController, + onDateSelected: (date) { + startController.text = + DateFormat('dd-MM-yyyy').format(date); + }, + ), + ); + } + + Widget buildEndDate(BuildContext context) { + return SizedBox( + height: 36, + width: ResponsiveLayout.isMobile(context) + ? null + : MediaQuery.of(context).size.width * 0.10, + child: ThemedDateField( + hintText: "End Date", + controller: endController, + lastDate: DateTime.now(), + onDateSelected: (date) { + endController.text = + DateFormat('dd-MM-yyyy').format(date); + // refresh(); + }, + ), + ); + } + + Widget buildRTOSearch(BuildContext context) { + final List> endorsementOptions = + List>.from(filteredEndrosmentData); + + Map? selectedItem; + + if (selectedEndorsementTypeVal != null && + selectedEndorsementTypeVal.toString().isNotEmpty && + endorsementOptions.isNotEmpty) { + try { + selectedItem = endorsementOptions.firstWhere( + (e) => + e['id'].toString() == + selectedEndorsementTypeVal.toString(), + ); + } catch (_) { + selectedItem = null; + } + } + + return SizedBox( + height: 33, + child: Container( + width: ResponsiveLayout.isMobile(context) + ? null + : MediaQuery.of(context).size.width * 0.10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: DropdownSearch>( + selectedItem: selectedItem, + + items: (filter, infiniteScrollProps) { + if (filter.isEmpty) return endorsementOptions; + + return endorsementOptions + .where((item) => item['endorsement_type'] + .toString() + .toLowerCase() + .contains(filter.toLowerCase())) + .toList(); + }, + + itemAsString: (item) => + item['endorsement_type']?.toString() ?? '', + + compareFn: (item, selected) { + if (selected == null) return false; + return item['id'].toString() == + selected['id'].toString(); + }, + + dropdownBuilder: (context, selectedItem) { + return Align( + alignment: Alignment.centerLeft, + child: Text( + selectedItem != null + ? selectedItem['endorsement_type'] + .toString() + : '', + style: GoogleFonts.poppins( + fontSize: 11, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + + decoratorProps: DropDownDecoratorProps( + decoration: + AppInputDecorations.dropdownDecoration( + label: "Insurer", + ).copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + contentPadding: + const EdgeInsets.symmetric( + horizontal: 5, + vertical: 1, + ), + ), + ), + + popupProps: PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, + constraints: BoxConstraints( + maxHeight: 300, + minWidth: ResponsiveLayout.isMobile(context) + ? 200 + : MediaQuery.of(context).size.width * 0.15, + ), + menuProps: + const MenuProps(backgroundColor: Colors.white), + ), + + onChanged: (val) { + if (val == null) return; + + selectedEndorsementTypeVal = val['id']; + // refresh(); + }, + ), + ), + ); + } + + Widget buildSelectInsurer(BuildContext context) { + // Ensure this list has data BEFORE building dropdown + final List> insurerOptions = + List>.from(filteredInsurerData); + + // Selected item logic + Map? selectedItem; + + if (selectedInsurerVal != null && + selectedInsurerVal.toString().isNotEmpty && + insurerOptions.isNotEmpty) { + try { + selectedItem = insurerOptions.firstWhere( + (e) => e['id'].toString() == selectedInsurerVal.toString(), + ); + } catch (e) { + selectedItem = null; + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 33, + child: Container( + width: ResponsiveLayout.isMobile(context) + ? null + : MediaQuery.of(context).size.width * 0.10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10 ), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: DropdownSearch>( + selectedItem: selectedItem, + + // FILTER LOGIC: This filters the list as the user types + items: (filter, infiniteScrollProps) { + if (filter.isEmpty) { + return insurerOptions; + } + return insurerOptions + .where((item) => item['short_name'] + .toString() + .toLowerCase() + .contains(filter.toLowerCase())) + .toList(); + }, + + itemAsString: (item) => item['short_name']?.toString() ?? '', + + compareFn: (item, selected) { + if (selected == null) return false; + return item['id'].toString() == selected['id'].toString(); + }, + + // FIELD UI (The collapsed state) + dropdownBuilder: (context, selectedItem) { + return Align( + alignment: Alignment.centerLeft, + child: Text( + selectedItem != null + ? selectedItem['short_name'].toString() + : '', + style: GoogleFonts.poppins( + fontSize: 11, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration( + label: "Select Insurer", + ).copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 1, + ), + ), + ), + + // POPUP UI (The expanded state with search) + popupProps: PopupProps.menu( + showSearchBox: true, // Enabled searching + fit: FlexFit.loose, + constraints: BoxConstraints( + maxHeight: 300, + minWidth: ResponsiveLayout.isMobile(context) + ? 200 + : MediaQuery.of(context).size.width * 0.10, + ), + menuProps: const MenuProps(backgroundColor: Colors.white), + + // Customizing the search field appearance + searchFieldProps: TextFieldProps( + style: GoogleFonts.poppins(fontSize: 13), + decoration: InputDecoration( + hintText: "Search insurer...", + hintStyle: GoogleFonts.poppins(fontSize: 12), + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + ), + ), + + itemBuilder: (context, item, isDisabled, isSelected) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + decoration: BoxDecoration( + color: isSelected ? Colors.blue.withOpacity(0.1) : Colors.transparent, + ), + child: Text( + item['short_name'].toString(), + style: GoogleFonts.inter( + fontSize: 13, + color: Colors.black, + ), + ), + ); + }, + ), + + // ON CHANGE + onChanged: (val) { + if (val == null) return; + + selectedInsurerVal = val['id']; + // refresh(); + }, + ), + ), + ), + ], + ); + } + + + + Widget buildplan(BuildContext context) { + final List> verificationOptions = [ + {'id': 2, 'status': 'All'}, + {'id': 0, 'status': 'To Verify'}, + {'id': 1, 'status': 'Verified'}, + ]; + + Map? selectedItem; + + if (selectedVerificationVal != null) { + try { + selectedItem = verificationOptions.firstWhere( + (e) => e['id'] == selectedVerificationVal, + ); + } catch (_) { + selectedItem = null; + } + } + + return SizedBox( + height: 33, + child: Container( + width: ResponsiveLayout.isMobile(context) + ? null + : MediaQuery.of(context).size.width * 0.10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: DropdownSearch>( + selectedItem: selectedItem, + items: (filter, infiniteScrollProps) => verificationOptions, + + itemAsString: (val) => val['status'].toString(), + + compareFn: (item, selected) { + if (selected == null) return false; + return item['id'] == selected['id']; + }, + + dropdownBuilder: (context, selectedItem) { + return Align( + alignment: Alignment.centerLeft, + child: Text( + selectedItem != null + ? selectedItem['status'].toString() + : '', + style: GoogleFonts.poppins( + fontSize: 11, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + + decoratorProps: DropDownDecoratorProps( + decoration: AppInputDecorations.dropdownDecoration( + label: "Select Verification", + ).copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 1, + ), + ), + ), + + popupProps: PopupProps.menu( + showSearchBox: false, + fit: FlexFit.loose, + constraints: BoxConstraints( + maxHeight: 300, + minWidth: ResponsiveLayout.isMobile(context) + ? 200 + : MediaQuery.of(context).size.width * 0.10, + ), + menuProps: const MenuProps(backgroundColor: Colors.white), + ), + + onChanged: (val) { + if (val == null) return; + + selectedVerificationVal = val['id']; + // refresh(); + }, + ), + ), + ); + } + + Widget _iconButton( + IconData icon, VoidCallback? onTap) { + return InkWell( + onTap: onTap, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFE5E7EB)), + color: Colors.white, + ), + child: Icon(icon, + size: 18, color: Colors.grey[700]), + ), + ); + } + + Widget _rightSearchField() { + return Container( + width: 220, + height: 36, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFE5E7EB)), + color: Colors.white, + ), + child: Row( + children: [ + const Icon(Icons.search, size: 18, color: Colors.grey), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: rightSearchController, + decoration: InputDecoration( + hintText: "Search...", + border: InputBorder.none, + isDense: true, + hintStyle: GoogleFonts.poppins(fontSize: 11), + ), + style: GoogleFonts.poppins(fontSize: 11), + onChanged: (val) { + filterData(val); + }, + ), + ), + ], + ), + ); + } + + Widget _exportButton() { + return InkWell( + onTap: () async { + await fetchGRidExcelReport(); + }, + child: Container( + width: 40, + height: 36, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: const Color(0xFF2E7D6E), + ), + child:Tooltip( + message: 'Export', + child: Image.asset( + "assets/miscellaneous/export.png", + height: 13, + width: 13, + ), + ), + ), + ); + } + + Widget _actionIconButton({ + required BuildContext context, + IconData? icon, + Widget? customIcon, + Widget Function(BuildContext buttonContext)? builderIcon, + required String tooltip, + required Color iconColor, + required VoidCallback? onTap, + Color? hoverColor, // ✅ Add this parameter + }) { + if (builderIcon != null) { + return Builder(builder: (buttonContext) => builderIcon(buttonContext)); + } + + return Material( + color: Colors.transparent, + child: Tooltip( + message: tooltip, + waitDuration: const Duration(milliseconds: 300), + showDuration: const Duration(seconds: 2), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + hoverColor: hoverColor ?? Colors.grey.shade200, // ✅ default grey + child: Padding( + padding: const EdgeInsets.all(5), + child: customIcon ?? Icon(icon, size: 15, color: iconColor), + ), + ), + ), + ); + } + +void _openViewModal(Map row) { + showDialog( + context: context, + builder: (context) => _PayoutRowViewDialog(row: row), + ); + } + + void _openEditModal(Map row) { + showDialog( + context: context, + builder: (context) => _PayoutRowEditDialog( + initialRow: row, + canEdit: true, + onSave: (updated) { + setState(() { + final id = updated['id']; + final idx = _rows.indexWhere( + (e) => e['id']?.toString() == id?.toString()); + if (idx >= 0) { + _rows[idx] = updated; + } else { + _rows.insert(0, updated); + } + }); + ToastHelper.showSuccessToast(context, 'Updated successfully'); + }, + ), + ); + } + + void _openAddModal() { + showDialog( + context: context, + builder: (context) => const UploadGridModal(), + ).then((result) async { + // `UploadGridModal` will pop with `true` after successful upload. + if (result == true && mounted) { + await _fetchGrid(); + } + }); + } + + Future _exportExcel() async { + setState(() => _loading = true); + try { + await _apiService.downloadgridViewInExcel( + startDate: null, + endDate: null, + endorsementType: _selectedEndorsementType, + insurer: _selectedInsurer, + vehicleNo: _selectedVehicle, + search: _searchValue.trim().isEmpty ? null : _searchValue.trim(), + ); + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + final queryRole = Uri.base.queryParameters['role']; + final canEdit = + (queryRole?.toLowerCase() == 'manager') || _isManagerOrAccount; + + + + + + String formatDateForList(String? dateStr) { + if (dateStr == null || dateStr.isEmpty || dateStr == '-') return '-'; + + try { + // Catch invalid dates like "0000-00-00" or "30-11--0001" + if (dateStr.contains('0000') || dateStr.startsWith('30-11--')) return '-'; + + DateTime parsed = DateTime.parse(dateStr); + + // Validate year is reasonable + if (parsed.year < 2000 || parsed.year > 2100) return '-'; + + return DateFormat('dd-MM-yyyy').format(parsed); + } catch (e) { + return '-'; + } + } + + + static final _dataBold = GoogleFonts.inter( + fontSize: 11, + + fontWeight: FontWeight.w400, + color: Color(0xFF000000), + ); + + static final _dataSub = GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w300, + color: Color(0xFF585757), + ); + + static final _headerStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: Color(0xFF1E293B), + ); + static final _cardheaderStyle = GoogleFonts.inter( + color: Colors.black, + fontWeight: FontWeight.w600, + fontSize: 12, + ); + + static final _cardBodyStyle = GoogleFonts.inter( + color: const Color(0xFF545454), + fontWeight: FontWeight.w400, + fontSize: 12, + ); +} + + + +// ══════════════════════════════════════════════════════════════════════════════ +// ADD PAYOUT GRID DIALOG – matches Image 1 (Incentive Files modal style) +// ══════════════════════════════════════════════════════════════════════════════ +class _gridViewAddDialog extends ConsumerStatefulWidget { + final Future Function() onAdded; + + const _gridViewAddDialog({required this.onAdded}); + + @override + ConsumerState<_gridViewAddDialog> createState() => + _gridViewAddDialogState(); +} + +class _gridViewAddDialogState + extends ConsumerState<_gridViewAddDialog> { + final ApiService _apiService = ApiService(); + + bool _submitting = false; + DateTime? _validFrom; + PlatformFile? _selectedFile; + + final TextEditingController _filesSearchController = + TextEditingController(); + String _filesSearchValue = ''; + + void _refresh() { + if (_submitting) return; + setState(() { + _validFrom = null; + _selectedFile = null; + _filesSearchValue = ''; + _filesSearchController.clear(); + }); + } + + String _formatDateForApi(DateTime date) => + date.toIso8601String().split('T').first; + + @override + void dispose() { + _filesSearchController.dispose(); + super.dispose(); + } + + Future _pickDate() async { + final picked = await showDatePicker( + context: context, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + initialDate: _validFrom ?? DateTime.now(), + ); + if (picked == null) return; + setState(() => _validFrom = picked); + } + + Future _pickXLFile() async { + final picked = await FilePicker.platform.pickFiles( + allowMultiple: false, + type: FileType.custom, + allowedExtensions: const ['xls', 'xlsx', 'csv'], + withData: true, + ); + if (picked == null || picked.files.isEmpty) return; + setState(() => _selectedFile = picked.files.first); + } + + Future _submit() async { + if (_validFrom == null) { + ToastHelper.showErrorToast(context, 'Select valid from date'); + return; + } + if (_selectedFile == null) { + ToastHelper.showErrorToast(context, 'Upload an XL file'); + return; + } + + setState(() => _submitting = true); + try { + // Requirement: hardcode agent_id to 1 for payout grid upload. + final agentId = 1; + final currentUserId = ref.read(userIdProvider); + + final payload = { + 'agent_id': agentId, + // Backend expects the uploader/creator user id. + // (Other uploads like incentive use the logged-in user id.) + 'created_by': currentUserId ?? agentId, + // Backend expects file_type as "grid" only. + 'file_type': 'grid', + // Backend expects date under `incentive_month`. + 'incentive_month': _formatDateForApi(_validFrom!), + }; + + final res = await _apiService.uploadgridViewFile( + file: _selectedFile!, + data: payload, + ); + + if ((res['status'] ?? '').toString().toLowerCase() == 'success') { + Navigator.pop(context); + await widget.onAdded(); + } else { + ToastHelper.showErrorToast( + context, res['data']?.toString() ?? 'Upload failed'); + } + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + + + + + Widget _buildSelectedFilesGrid() { + final files = _getSelectedFiles(); + final q = _filesSearchValue.trim().toLowerCase(); + + final filtered = q.isEmpty + ? files + : files.where((f) { + final name = (f['file_name'] ?? '').toString().toLowerCase(); + final month = (f['month'] ?? '').toString().toLowerCase(); + return name.contains(q) || month.contains(q); + }).toList(); + + if (filtered.isEmpty) { + return const Center(child: Text('No files found')); + } + + return LayoutBuilder( + builder: (context, constraints) { + final fileAreaWidth = constraints.maxWidth; + final crossAxisCount = fileAreaWidth < 520 ? 1 : 2; + const spacing = 10.0; + const targetTileHeight = 70.0; + + final tileWidth = (fileAreaWidth - + spacing * (crossAxisCount - 1) - + 16) / + crossAxisCount; + final aspectRatio = (tileWidth / targetTileHeight).clamp(3.2, 12.0); + + return GridView.builder( + padding: const EdgeInsets.all(8), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: spacing, + mainAxisSpacing: spacing, + childAspectRatio: aspectRatio, + ), + itemCount: filtered.length, + itemBuilder: (context, index) { + final f = filtered[index]; + return gridViewFileRow( + fileName: f['file_name']?.toString() ?? 'Unknown', + date: f['month']?.toString() ?? '-', + onUpload: () async { + try { + await _apiService.downloadLatestgridView(); + } catch (_) {} + }, + onDelete: _refresh, + ); + }, + ); + }, + ); + } + + InputDecoration _inputDecoration(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: GoogleFonts.inter( + fontSize: 12, color: const Color(0xFFCBD5E1)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide(color: Color(0xFFE2E8F0)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide(color: Color(0xFFE2E8F0)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: + const BorderSide(color: Color(0xFF2E7D6E), width: 1.5), + ), + filled: true, + fillColor: Colors.white, + ); + } +} + +final _labelStyle = GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 12, +); + +class gridViewFileRow extends StatelessWidget { + final String fileName; + final String date; + final VoidCallback? onUpload; + final VoidCallback? onDelete; + + const gridViewFileRow({ + super.key, + required this.fileName, + required this.date, + this.onUpload, + this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE3E3E3)), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6.0), + decoration: BoxDecoration( + color: const Color(0xFFF4F6F8), + borderRadius: BorderRadius.circular(6.0), + border: Border.all(color: const Color(0xFFE3E3E3)), + ), + child: const Icon( + Icons.file_present, + color: Color(0xFF838587), + size: 18, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + fileName, + style: GoogleFonts.poppins( + fontWeight: FontWeight.w400, + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(height: 2), + Text( + date, + style: GoogleFonts.poppins( + color: const Color(0xFF6E6E6E), + fontSize: 10, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onUpload, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon( + Icons.file_download_outlined, + color: Color(0xFF6E6E6E), + size: 20, + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onDelete, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon( + Icons.delete_outlined, + color: Color(0xFF6E6E6E), + size: 20, + ), + ), + ), + ), + ], + ), + ); + } +} + +// ══════════════════════════════════════════════════════════════════════════════ +// VIEW DIALOG +// ══════════════════════════════════════════════════════════════════════════════ +class _PayoutRowViewDialog extends StatelessWidget { + final Map row; + + const _PayoutRowViewDialog({required this.row}); + + String _val(dynamic v) { + final s = v?.toString().trim() ?? ''; + return s.isEmpty ? '-' : s; + } + + @override + Widget build(BuildContext context) { + const fields = [ + 'id', 'vehicle_type', 'fuel', 'insurer', 'rto', 'broker_name', + 'segment', 'comp', 'tp', 'od', 'remarks', + 'partner_comp', 'partner_tp', 'partner_od', + ]; + + final labels = { + 'id': 'ID', 'vehicle_type': 'Vehicle Type', 'fuel': 'Fuel', + 'insurer': 'Insurer', 'rto': 'RTO', 'broker_name': 'Broker', + 'segment': 'Segment', 'comp': 'Comp', 'tp': 'TP', 'od': 'OD', + 'remarks': 'Remarks', 'partner_comp': 'Partner Comp', + 'partner_tp': 'Partner TP', 'partner_od': 'Partner OD', + }; + + return AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + title: Text('Payout Grid Details', + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: const Color(0xFF50A398))), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: fields.map((k) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 110, + child: Text('${labels[k] ?? k}:', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: const Color(0xFF475569))), + ), + Expanded( + child: Text(_val(row[k]), + style: GoogleFonts.inter( + fontSize: 12, color: Colors.black87)), + ), + ], + ), + ); + }).toList(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Close', + style: GoogleFonts.inter( + fontSize: 12, color: const Color(0xFF2E7D6E))), + ), + ], + ); + } +} + +// ══════════════════════════════════════════════════════════════════════════════ +// EDIT DIALOG +// ══════════════════════════════════════════════════════════════════════════════ +class _PayoutRowEditDialog extends StatefulWidget { + final Map initialRow; + final bool isNew; + final bool canEdit; + final ValueChanged> onSave; + + const _PayoutRowEditDialog({ + super.key, + required this.initialRow, + this.isNew = false, + required this.canEdit, + required this.onSave, + }); + + @override + State<_PayoutRowEditDialog> createState() => _PayoutRowEditDialogState(); +} + +class _PayoutRowEditDialogState extends State<_PayoutRowEditDialog> { + late final Map _ctrls; + + @override + void initState() { + super.initState(); + final r = widget.initialRow; + _ctrls = { + 'id': TextEditingController( + text: r['id']?.toString().trim().isNotEmpty == true + ? r['id'].toString() + : DateTime.now().millisecondsSinceEpoch.toString()), + 'vehicle_type': TextEditingController( + text: r['vehicle_type']?.toString() ?? ''), + 'fuel': TextEditingController(text: r['fuel']?.toString() ?? ''), + 'insurer': TextEditingController( + text: r['insurer']?.toString() ?? ''), + 'rto': TextEditingController(text: r['rto']?.toString() ?? ''), + 'broker_name': TextEditingController( + text: r['broker_name']?.toString() ?? ''), + 'segment': TextEditingController( + text: r['segment']?.toString() ?? ''), + 'comp': TextEditingController(text: r['comp']?.toString() ?? ''), + 'tp': TextEditingController(text: r['tp']?.toString() ?? ''), + 'od': TextEditingController(text: r['od']?.toString() ?? ''), + 'remarks': TextEditingController( + text: r['remarks']?.toString() ?? ''), + 'partner_comp': TextEditingController( + text: r['partner_comp']?.toString() ?? ''), + 'partner_tp': TextEditingController( + text: r['partner_tp']?.toString() ?? ''), + 'partner_od': TextEditingController( + text: r['partner_od']?.toString() ?? ''), + }; + } + + @override + void dispose() { + for (final c in _ctrls.values) { + c.dispose(); + } + super.dispose(); + } + + dynamic _val(String key) { + final t = _ctrls[key]!.text.trim(); + return t.isEmpty ? null : t; + } + + @override + Widget build(BuildContext context) { + final title = + widget.isNew ? 'Add Payout Grid' : 'Edit Payout Grid'; + + final fieldDefs = [ + ('ID', 'id', false, 1), + ('Vehicle Type', 'vehicle_type', true, 1), + ('Fuel', 'fuel', true, 1), + ('Insurer', 'insurer', true, 1), + ('RTO', 'rto', true, 1), + ('Broker', 'broker_name', true, 1), + ('Segment', 'segment', true, 1), + ('Comp', 'comp', true, 1), + ('TP', 'tp', true, 1), + ('OD', 'od', true, 1), + ('Remarks', 'remarks', true, 3), + ('Partner Comp', 'partner_comp', true, 1), + ('Partner TP', 'partner_tp', true, 1), + ('Partner OD', 'partner_od', true, 1), + ]; + + return AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + title: Text(title, + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: const Color(0xFF50A398))), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 500), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: fieldDefs.map((def) { + final (label, key, enabled, maxLines) = def; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: TextField( + controller: _ctrls[key], + enabled: enabled, + maxLines: maxLines, + style: GoogleFonts.inter(fontSize: 12), + decoration: InputDecoration( + labelText: label, + labelStyle: GoogleFonts.inter(fontSize: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6)), + isDense: true, + ), + ), + ); + }).toList(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel', + style: GoogleFonts.inter( + fontSize: 12, color: const Color(0xFF64748B))), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2E7D6E), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6)), + ), + onPressed: widget.canEdit + ? () { + final updated = { + for (final k in _ctrls.keys) k: _val(k), + }; + widget.onSave(updated); + Navigator.pop(context); + } + : null, + child: + Text('Save', style: GoogleFonts.inter(fontSize: 12)), + ), + ], + ); + } +} +*/ \ No newline at end of file diff --git a/lib/presentation/screens/UserManagement/Agent/agentIncentiveFile.dart b/lib/presentation/screens/UserManagement/Agent/agentIncentiveFile.dart index 9a2c772..c6f1b19 100644 --- a/lib/presentation/screens/UserManagement/Agent/agentIncentiveFile.dart +++ b/lib/presentation/screens/UserManagement/Agent/agentIncentiveFile.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; -import 'package:intl/intl.dart'; import 'package:nhance_partner/data/utils/toastNotification.dart'; import 'package:nhance_partner/presentation/themes/indicators/text_field_theme.dart'; import 'package:toastification/toastification.dart'; @@ -20,7 +19,6 @@ import '../../../../data/services/auth_service.dart'; import '../../../../data/utils/validators.dart'; import '../../../providers/manager_provider.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/month_field_theme.dart'; import '../../../themes/indicators/search_field_theme.dart'; @@ -56,7 +54,7 @@ class UploadIncentiveModalState extends ConsumerState { Map controllers = {}; // html.File? passportFile; - PlatformFile? passportFile; + List selectedFiles = []; String? selectedFileNames; List tabHeader = ['name', 'agentId', 'date', 'search']; @@ -65,10 +63,12 @@ class UploadIncentiveModalState extends ConsumerState { dynamic managerId; dynamic agentId; Map dataDetails() { + final selectedDate = controllers["date"]?.text; final data = { "agent_id": agentId, - "incentive_month": controllers["date"]?.text, + "incentive_month": selectedDate, "created_by": userId, + "file_type": "incentive", }; return data; } @@ -110,7 +110,7 @@ class UploadIncentiveModalState extends ConsumerState { Future refresh() async { setState(() { // reset file fields - passportFile = null; + selectedFiles = []; // selectedFileNames = ""; selectedFileNames = null; @@ -179,7 +179,7 @@ class UploadIncentiveModalState extends ConsumerState { }); try { - final response = await apiService.fetchAgentIncentiveList(agentId); + final response = await apiService.fetchAgentIncentiveList(agentId, type: 'incentive'); if (response['status'] == 'success') { print('AgentIncentive - ${response['data']}'); @@ -214,7 +214,7 @@ class UploadIncentiveModalState extends ConsumerState { print("dataSetAgent - $dataSet"); print("Handl13f"); - if (passportFile == null) { + if (selectedFiles.isEmpty) { print("❌ No file selected!"); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -257,26 +257,28 @@ class UploadIncentiveModalState extends ConsumerState { } }); - if (passportFile != null) { - try { - if (passportFile!.bytes != null) { - final multipartFile = http.MultipartFile.fromBytes( - 'incentive_file_name', - passportFile!.bytes!, - filename: passportFile!.name, - ); - request.files.add(multipartFile); - } else if (passportFile!.path != null) { - final multipartFile = await http.MultipartFile.fromPath( - 'certificate_file_name', - passportFile!.path!, - filename: passportFile!.name, - ); - request.files.add(multipartFile); + if (selectedFiles.isNotEmpty) { + for (final file in selectedFiles) { + try { + if (file.bytes != null) { + final multipartFile = http.MultipartFile.fromBytes( + 'incentive_file_name', + file.bytes!, + filename: file.name, + ); + request.files.add(multipartFile); + } else if (file.path != null) { + final multipartFile = await http.MultipartFile.fromPath( + 'incentive_file_name', + file.path!, + filename: file.name, + ); + request.files.add(multipartFile); + } + print("📎 File attached: ${file.name}"); + } catch (e) { + print("❌ Failed to attach file ${file.name}: $e"); } - print("📎 File attached: ${passportFile!.name}"); - } catch (e) { - print("❌ Failed to attach file: $e"); } } @@ -341,28 +343,23 @@ class UploadIncentiveModalState extends ConsumerState { } else { print("❌ Submission failed. Status: ${response.statusCode}"); print("📨 Body: ${response.body}"); + String errorMessage = 'There was a problem in creating user. Please try again.'; + try { + if (responseBody['data'] is String && + (responseBody['data'] as String).isNotEmpty) { + final parsedData = json.decode(responseBody['data']); + if (parsedData is Map && + parsedData['message'] != null) { + errorMessage = parsedData['message'].toString(); + } + } else if (responseBody['message'] != null) { + errorMessage = responseBody['message'].toString(); + } + } catch (_) { + // keep fallback message + } - // Sort by created_on (latest first) - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text("Partner Creation Failed"), - content: Text( - "There was a problem in creating user. Please try again.", - ), - actions: [ - TextButton( - child: Text("OK"), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); + ToastHelper.showErrorToast(context, errorMessage); } } catch (e) { print("🔥 Error submitting user: $e"); @@ -371,12 +368,19 @@ class UploadIncentiveModalState extends ConsumerState { @override Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final isCompact = screenWidth < 1000; + final dialogWidth = isCompact ? screenWidth * 0.95 : screenWidth * 0.55; + final dialogHeight = isCompact ? screenHeight * 0.82 : screenHeight * 0.7; + final fieldWidth = isCompact ? dialogWidth * 0.9 : screenWidth * 0.27; + return SelectionArea( child:AlertDialog( backgroundColor: Colors.white, content: Container( - width: MediaQuery.of(context).size.width * 0.55, - height: MediaQuery.of(context).size.height * 0.7, + width: dialogWidth, + height: dialogHeight, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -431,7 +435,141 @@ class UploadIncentiveModalState extends ConsumerState { ], ), const SizedBox(height: 5), - Row( + isCompact + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Partner Name', style: _labelStyle), + SizedBox(height: 5), + Container( + width: fieldWidth, + height: 40, + child: DropdownSearch>( + key: dropDownKey, + selectedItem: null, + items: (filter, infiniteScrollProps) { + return filteredData; // pass the whole object + }, + itemAsString: (agent) => + agent['name'].toString(), // what to show + compareFn: (item, selectedItem) => + item['id'] == + selectedItem['id'], // ✅ compare by id + decoratorProps: DropDownDecoratorProps( + decoration: + AppInputDecorations.dropdownDecoration( + label: "Select Partner", + ).copyWith( + hintStyle: GoogleFonts.inter( + fontSize: 12, + color: Colors.black, + ), + filled: true, + fillColor: Colors.white, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xFFE2E8F0), + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xFFE2E8F0), + width: 1.5, + ), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 8, + vertical: 6, + ), + ), + ), + popupProps: PopupProps.menu( + fit: FlexFit.loose, + menuProps: + MenuProps(backgroundColor: Colors.white), + showSearchBox: true, + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Search partner...", + hintStyle: GoogleFonts.inter( + fontSize: 12, + color: Colors.black, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.white, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xFFEDF6F5), + width: 1, + ), + ), + ), + ), + itemBuilder: + ( + context, + item, + isDisabled, + isSelected, + ) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + child: Text( + item['name'].toString(), + style: GoogleFonts.inter( + fontSize: 12, + color: Colors.black, + ), + ), + ); + }, + ), + onChanged: (agent) { + if (agent != null) { + print("Selected Partner Name: ${agent['name']}"); + print("Agent Code: ${agent['agent_code']}"); + print("Agent Id: ${agent['id']}"); + getAgentIncenctiveFileList(agent['id']); + controllers['agentId']?.text = + agent['agent_code']; + agentId = agent['id']; + } + }, + ), + ), + ], + ), + SizedBox(height: 10), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Partner ID', style: _labelStyle), + SizedBox(height: 5), + ThemedFormField( + controller: controllers['agentId']!, + txtwidth: fieldWidth, + txtheight: 40, + borderColor: Color(0xFFE2E8F0), + highlightColor: Color(0xFF50A398), + readOnly: true, + ), + ], + ), + ], + ) + : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( @@ -442,7 +580,7 @@ class UploadIncentiveModalState extends ConsumerState { SizedBox(height: 5), Container( - width: MediaQuery.of(context).size.width * 0.27, + width: fieldWidth, height: 40, child: DropdownSearch>( key: dropDownKey, @@ -559,7 +697,7 @@ class UploadIncentiveModalState extends ConsumerState { SizedBox(height: 5), ThemedFormField( controller: controllers['agentId']!, - txtwidth: MediaQuery.of(context).size.width * 0.27, + txtwidth: fieldWidth, txtheight: 40, borderColor: Color(0xFFE2E8F0), highlightColor: Color(0xFF50A398), @@ -587,14 +725,15 @@ class UploadIncentiveModalState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Month', style: _labelStyle), + Text( + 'Month', + style: _labelStyle, + ), SizedBox(height: 5), ThemedMonthField( hintText: "Select Month", txtwidth: MediaQuery.of(context).size.width * 0.27, txtheight: 40, - - // backgroundColor: const Color(0xFFECECEC), validator: (value) => Validators.requiredField(value, "date"), borderColor: Color(0xFFE2E8F0), @@ -602,7 +741,6 @@ class UploadIncentiveModalState extends ConsumerState { controller: controllers['date']!, onDateSelected: (date) { print("Picked Date: $date"); - // controllers['date']?.text = date as String; }, ), ], @@ -612,7 +750,11 @@ class UploadIncentiveModalState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Upload File', style: _labelStyle), + Row( + children: [ + Text('Upload Files', style: _labelStyle), + ], + ), SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -620,6 +762,7 @@ class UploadIncentiveModalState extends ConsumerState { children: [ ThemedUploadField( key: ValueKey(selectedFileNames), + allowMultiple: true, // hintText: "Upload Document", txtheight: 40, // validator: (value) => Validators.requiredField( @@ -635,30 +778,33 @@ class UploadIncentiveModalState extends ConsumerState { // backgroundColor: Color(0xFFECECEC), backgroundColor: Colors.white, borderColor: Color(0xFFE2E8F0), + allowedExtensions: null, hintText: (selectedFileNames == null || selectedFileNames!.isEmpty) ? "Upload Document" : selectedFileNames, - txtwidth: - MediaQuery.of(context).size.width * 0.19, - onFileSelected: (fileName, file) { - print("File picked: ${fileName}"); + txtwidth: isCompact + ? fieldWidth * 0.71 + : MediaQuery.of(context).size.width * 0.19, + onFilesSelected: (fileNames, files) { + print("Files picked: ${fileNames.join(', ')}"); // print("Size: ${file.size}"); // print( // "Path: ${file.path}", // ); // works on mobile/desktop // print("Bytes: ${file.bytes}"); setState(() { - passportFile = file; - selectedFileNames = fileName; + selectedFiles = files; + selectedFileNames = fileNames.join(', '); }); }, ), SizedBox(width: 15), Container( - width: - MediaQuery.of(context).size.width * 0.068, + width: isCompact + ? fieldWidth * 0.25 + : MediaQuery.of(context).size.width * 0.068, child: ElevatedButton( style: ElevatedButton.styleFrom( // backgroundColor: Color(0xFF425B5B), @@ -713,68 +859,68 @@ class UploadIncentiveModalState extends ConsumerState { child: isLoading ? const Center(child: CircularProgressIndicator()) : filteredIncentiveData.isEmpty - ? const Center(child: Text("No incentive files found")) - : GridView.builder( - padding: const EdgeInsets.all(8), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, // two columns - crossAxisSpacing: 10, - mainAxisSpacing: 10, - childAspectRatio: 6.5, // adjust height of rows + ? const Center(child: Text("No files found")) + : LayoutBuilder( + builder: (context, constraints) { + final fileAreaWidth = constraints.maxWidth; + final crossAxisCount = + fileAreaWidth < 520 ? 1 : 2; + const spacing = 10.0; + final tileWidth = (fileAreaWidth - + spacing * (crossAxisCount - 1) - + 16) / + crossAxisCount; + const targetTileHeight = 70.0; + final aspectRatio = (tileWidth / targetTileHeight) + .clamp(3.2, 12.0); + + return GridView.builder( + padding: const EdgeInsets.all(8), + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: spacing, + mainAxisSpacing: spacing, + childAspectRatio: aspectRatio, ), - itemCount: filteredIncentiveData.length, - itemBuilder: (context, index) { - final file = filteredIncentiveData[index]; + itemCount: filteredIncentiveData.length, + itemBuilder: (context, index) { + final file = filteredIncentiveData[index]; + return IncentiveFileRow( + fileName: + file['incentive_file_name'] ?? "Unknown", + date: file['incentive_month'] ?? "-", + onUpload: () { + print("Upload ${file['id']}"); + final selectedId = file['id']; + final path = + 'agent/downloadAgentIncentiveFile?id=$selectedId'; + apiService.getPdfDownload(path, selectedId); + }, + onDelete: () async { + print("Delete ${file['id']}"); + final selectedId = file['id']; + final response = await apiService + .deleteAgentIncentiveFile(selectedId); + print("res - $response"); - // final sortedData = [...filteredIncentiveData] - // ..sort((a, b) { - // final dateA = - // DateTime.tryParse(a['created_on'] ?? '') ?? - // DateTime(1970); - // final dateB = - // DateTime.tryParse(b['created_on'] ?? '') ?? - // DateTime(1970); - // return dateB.compareTo(dateA); // latest first - // }); - // final file = sortedData[index]; - return IncentiveFileRow( - fileName: file['incentive_file_name'] ?? "Unknown", - date: file['incentive_month'] ?? "-", - onUpload: () { - print("Upload ${file['id']}"); - final selectedId = file['id']; - final path = - 'agent/downloadAgentIncentiveFile?id=$selectedId'; - apiService.getPdfDownload(path, selectedId); - }, - onDelete: () async { - print("Delete ${file['id']}"); - final selectedId = file['id']; - final response = await apiService - .deleteAgentIncentiveFile(selectedId); - print("res - $response"); + if (response['status'] == 'success') { + print("DELTED"); - if (response['status'] == 'success') { - print("DELTED"); + final data = + dataDetails(); // Map + final agentId = data["agent_id"]; - final data = - dataDetails(); // this is a Map - final agentId = data["agent_id"]; - - getAgentIncenctiveFileList(agentId); - // Fluttertoast.showToast( - // msg: response['data'] ?? "Success", - // toastLength: Toast.LENGTH_SHORT, - // gravity: ToastGravity.BOTTOM, - // ); - } else { - // Fluttertoast.showToast( - // msg: "Something went wrong", - // toastLength: Toast.LENGTH_SHORT, - // gravity: ToastGravity.BOTTOM, - // ); - } + getAgentIncenctiveFileList(agentId); + } else { + // Fluttertoast.showToast( + // msg: "Something went wrong", + // toastLength: Toast.LENGTH_SHORT, + // gravity: ToastGravity.BOTTOM, + // ); + } + }, + ); }, ); }, @@ -814,17 +960,14 @@ class IncentiveFileRow extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(6.0), - // margin: const EdgeInsets.only(bottom: 5), // spacing between rows + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( - // color: Colors.amber.shade50, border: Border.all(color: const Color(0xFFE3E3E3)), + borderRadius: BorderRadius.circular(6), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ Container( padding: const EdgeInsets.all(6.0), @@ -840,47 +983,60 @@ class IncentiveFileRow extends StatelessWidget { ), ), const SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 100, - child: Text( + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( fileName, style: GoogleFonts.poppins( fontWeight: FontWeight.w400, fontSize: 12, ), overflow: TextOverflow.ellipsis, - softWrap: true, maxLines: 1, ), - ), - Text( - date, - style: GoogleFonts.poppins( - color: Color(0xFF6E6E6E), - fontSize: 10, + const SizedBox(height: 2), + Text( + date, + style: GoogleFonts.poppins( + color: const Color(0xFF6E6E6E), + fontSize: 10, + ), ), - ), - ], - ), - const Spacer(), - GestureDetector( - onTap: onUpload, - child: const Icon( - Icons.file_download_outlined, - color: Color(0xFF6E6E6E), - size: 18, + ], ), ), - SizedBox(width: 10), - GestureDetector( - onTap: onDelete, - child: const Icon( - Icons.delete_outlined, - color: Color(0xFF6E6E6E), - size: 18, + const SizedBox(width: 8), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onUpload, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon( + Icons.file_download_outlined, + color: Color(0xFF6E6E6E), + size: 20, + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onDelete, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon( + Icons.delete_outlined, + color: Color(0xFF6E6E6E), + size: 20, + ), + ), ), ), ], diff --git a/lib/presentation/screens/UserManagement/Agent/agentList.dart b/lib/presentation/screens/UserManagement/Agent/agentList.dart index e58b51a..569de5c 100644 --- a/lib/presentation/screens/UserManagement/Agent/agentList.dart +++ b/lib/presentation/screens/UserManagement/Agent/agentList.dart @@ -108,6 +108,9 @@ class AgentListState extends ConsumerState { (item['agent_code'] ?? '-').toLowerCase().contains( query.toLowerCase(), ) || + (item['sales_executive_name'] ?? '-').toLowerCase().contains( + query.toLowerCase(), + ) || (item['retention_rate'] ?? '-').toLowerCase().contains( query.toLowerCase(), ) || @@ -423,6 +426,10 @@ class AgentListState extends ConsumerState { flex: 2, child: Text('Phone Number', style: _headerStyle), ), + Expanded( + flex: 2, + child: Text('Sales Executive Name', style: _headerStyle), + ), Expanded( flex: 1, child: Text( @@ -547,6 +554,10 @@ class AgentListState extends ConsumerState { flex: 2, child: Text(item['mobile'] ?? '-', style: _dataBold), ), + Expanded( + flex: 2, + child: Text(item['sales_executive_name'] ?? '-', style: _dataBold), + ), Expanded( flex: 1, child: Text(item['retention_rate'] ?? '-', style: _dataBold), diff --git a/lib/presentation/screens/UserManagement/Profile/profile_mobile.dart b/lib/presentation/screens/UserManagement/Profile/profile_mobile.dart index 5489aa7..36a8944 100644 --- a/lib/presentation/screens/UserManagement/Profile/profile_mobile.dart +++ b/lib/presentation/screens/UserManagement/Profile/profile_mobile.dart @@ -97,7 +97,7 @@ class ProfileState extends ConsumerState { if (roleId == 1) { response = await apiService.fetchManagerIncentiveList(id); } else { - response = await apiService.fetchAgentIncentiveList(id); + response = await apiService.fetchAgentIncentiveList(id, type: 'incentive'); } if (response['status'] == 'success') { @@ -349,7 +349,7 @@ class ProfileState extends ConsumerState { ) : filteredIncentiveData.isEmpty ? const Center( - child: Text("No incentive files found"), + child: Text("No files found"), ) : SizedBox( height: ResponsiveLayout.isMobile(context) @@ -449,7 +449,7 @@ class ProfileState extends ConsumerState { : null, child: isLoading ? const Center( - child: Text("No incentive files found"), + child: Text("No files found"), ) : Column( children: [ @@ -614,7 +614,7 @@ class ProfileState extends ConsumerState { ) : filteredIncentiveData.isEmpty ? const Center( - child: Text("No incentive files found"), + child: Text("No files found"), ) : SizedBox( height: ResponsiveLayout.isMobile(context) diff --git a/lib/presentation/screens/UserManagement/Profile/profile_web.dart b/lib/presentation/screens/UserManagement/Profile/profile_web.dart index c8890b4..ffb4d26 100644 --- a/lib/presentation/screens/UserManagement/Profile/profile_web.dart +++ b/lib/presentation/screens/UserManagement/Profile/profile_web.dart @@ -116,7 +116,7 @@ class ProfilePopUpState extends ConsumerState { if (roleId == 1) { response = await apiService.fetchManagerIncentiveList(id); } else { - response = await apiService.fetchAgentIncentiveList(id); + response = await apiService.fetchAgentIncentiveList(id, type: 'incentive'); } if (response['status'] == 'success') { diff --git a/lib/presentation/screens/UserManagement/Staff/staff.dart b/lib/presentation/screens/UserManagement/Staff/staff.dart index 0e3c102..f8906f7 100644 --- a/lib/presentation/screens/UserManagement/Staff/staff.dart +++ b/lib/presentation/screens/UserManagement/Staff/staff.dart @@ -67,21 +67,6 @@ class StaffState extends ConsumerState { dynamic userId; dynamic managerId; - Map dataDetails() { - final data = { - "name": controllers["name"]?.text, - "email": controllers["email"]?.text, - "mobile": controllers["mobile"]?.text, - // "emp_id": controllers["code"]?.text, - "is_active": isActive, - "role_id": selectedRole, - // "handler_id": selectedHandler, - "handler_id": selectedHandlerIds, - "manager_id": userId, - }; - return data; - } - @override void initState() { super.initState(); @@ -104,6 +89,24 @@ class StaffState extends ConsumerState { }); } + Map dataDetails() { + final data = { + "name": controllers["name"]?.text, + "email": controllers["email"]?.text, + "mobile": controllers["mobile"]?.text, + // "emp_id": controllers["code"]?.text, + "is_active": isActive, + "role_id": selectedRole, + // "handler_id": selectedHandler, + "handler_id": selectedHandlerIds, + // Always send mapped manager id: + // 1) Secondary Manager -> mapped Manager ID + // 2) All other roles -> mapped Manager ID + "manager_id": managerId, + }; + return data; + } + Future _initializeToken() async { _token = await AuthService.getToken(); print("APISERTOKEN - $_token"); @@ -172,6 +175,16 @@ class StaffState extends ConsumerState { return; } + // Prevent submit without mapped manager id. + if (managerId == null || managerId.toString().trim().isEmpty) { + print('Manager mapping missing. managerId: $managerId, userId: $userId'); + ToastHelper.showSuccessToast( + context, + 'Manager is missing. Please map a manager and try again.', + ); + return; + } + setState(() { if (_formKey.currentState!.validate()) { dataDetails(); diff --git a/lib/presentation/screens/dashboard/partner_portal_dashboard.dart b/lib/presentation/screens/dashboard/partner_portal_dashboard.dart new file mode 100644 index 0000000..a2c63b3 --- /dev/null +++ b/lib/presentation/screens/dashboard/partner_portal_dashboard.dart @@ -0,0 +1,788 @@ +// ───────────────────────────────────────────────────────────────────────────── +// partner_portal_dashboard.dart – Nhance Partner Portal +// ───────────────────────────────────────────────────────────────────────────── + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../../../core/services/api_service.dart'; +import '../../providers/manager_provider.dart'; +import '../../providers/userRoleProvider.dart'; +import '../../layouts/main_layout.dart'; + +String _currentFinYear() { + final now = DateTime.now(); + final s = now.month >= 4 ? now.year : now.year - 1; + return '$s-${s + 1}'; +} +List _allFinYears() { + final now = DateTime.now(); + final cur = now.month >= 4 ? now.year : now.year - 1; + return [for (int y = 2022; y <= cur; y++) '$y-${y + 1}']; +} +String _fyShort(String fy) { final p = fy.split('-'); return 'FY ${p[0].substring(2)}–${p[1].substring(2)}'; } +String _fyFull(String fy) { final p = fy.split('-'); return 'FY ${p[0]}–${p[1]}'; } +String _fyRange(String fy) { final p = fy.split('-'); return 'Apr ${p[0]} – Mar ${p[1]}'; } +DateTime _fyStart(String fy) => DateTime(int.parse(fy.split('-').first), 4, 1); +DateTime _fyEnd(String fy) => DateTime(int.parse(fy.split('-').last), 3, 31); + +class PartnerPortalDashboard extends ConsumerStatefulWidget { + const PartnerPortalDashboard({super.key}); + @override + ConsumerState createState() => _State(); +} + +class _State extends ConsumerState { + late ApiService api; + bool loading = false; + bool _isAgentRole = false; + int? _agentId; + + Map partnerDetails = {}; + List> allPolicies = []; + List> allRenewals = []; + List> filteredRenewals = []; + int selectedRenewalDays = 10, renewalPage = 0; + static const kPerPage = 4; + + List> allEarnings = []; + String selectedFY = _currentFinYear(); + + @override + void initState() { + super.initState(); + api = ApiService(); + Future.microtask(() { + final role = (ref.read(userRoleProvider) ?? '').toString().trim().toLowerCase(); + _isAgentRole = role == 'agent' || role == 'a'; + _agentId = ref.read(userIdProvider); + + /* + * Partner dashboard is only for Agent logins. + * Use userIdProvider as agent id (not managerIdProvider). + */ + if (_isAgentRole && _agentId != null) { + _loadAll(_agentId); + } + }); + } + + Future _loadAll(dynamic id) async { + setState(() => loading = true); + try { + await Future.wait([ + _fetchDetails(id), _fetchPolicies(id), + _fetchRenewals(id, days: selectedRenewalDays), _fetchEarnings(id), + ]); + } finally { setState(() => loading = false); } + } + + Future _fetchDetails(dynamic id) async { + final r = await api.getPartnerDetails(id); + if (r['status'] == 'success') setState(() => partnerDetails = Map.from(r['data'] ?? {})); + } + + Future _fetchPolicies(dynamic id) async { + final r = await api.getPartnerPolicies(id); + if (r['status'] == 'success') setState(() => allPolicies = List>.from((r['data'] ?? []).map((e) => Map.from(e)))); + } + + Future _fetchRenewals(dynamic id, {int days = 10}) async { + final r = await api.getPartnerRenewals(id, days: days); + if (r['status'] == 'success') { + final raw = List>.from((r['data'] ?? []).map((e) => Map.from(e))); + setState(() { + selectedRenewalDays = days; + allRenewals = raw; + filteredRenewals = raw; + renewalPage = 0; + }); + } + } + + Future _fetchEarnings(dynamic id) async { + final r = await api.getPartnerEarnings(id); + if (r['status'] == 'success') setState(() => allEarnings = List>.from((r['data'] ?? []).map((e) => Map.from(e)))); + } + + void _applyRenewalFilter(int days) { + setState(() { + selectedRenewalDays = days; + filteredRenewals = allRenewals.where((r) => (r['days_left'] ?? 999) <= days).toList(); + renewalPage = 0; + }); + } + + List> get _fyEarnings { + final s = _fyStart(selectedFY), e = _fyEnd(selectedFY); + return allEarnings.where((row) { + try { + final raw = row['month_key']?.toString() ?? ''; + final dt = raw.length == 7 ? DateTime.parse('$raw-01') : DateTime.tryParse(raw); + if (dt == null) return true; + return !dt.isBefore(s) && !dt.isAfter(e); + } catch (_) { return true; } + }).toList(); + } + + Map get _fyAgg { + double pre = 0, com = 0, tds = 0, net = 0; int pol = 0; bool allPaid = true; + for (final e in _fyEarnings) { + pre += _d(e['premium']); com += _d(e['commission']); + tds += _d(e['tds']); net += _d(e['net_payout']); + pol += int.tryParse(e['policies']?.toString() ?? '0') ?? 0; + if ((e['paid']?.toString()) != '1' && e['paid'] != true) allPaid = false; + } + if (com == 0 && pre > 0) { com = pre * 0.15; tds = com * 0.10; net = com - tds; } + return { 'premium': pre, 'commission': com, 'tds': tds, 'netPayout': net, 'policies': pol, 'paid': allPaid && _fyEarnings.isNotEmpty }; + } + + double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0; + + List> get _pageRenewals { + final s = renewalPage * kPerPage; + final e = (s + kPerPage).clamp(0, filteredRenewals.length); + return s >= filteredRenewals.length ? [] : filteredRenewals.sublist(s, e); + } + int get _totalPages => (filteredRenewals.length / kPerPage).ceil(); + + String _inr(dynamic v) { + final n = _d(v); + if (n >= 10000000) return 'Rs.${(n/10000000).toStringAsFixed(1)}Cr'; + if (n >= 100000) return 'Rs.${(n/100000).toStringAsFixed(1)}L'; + if (n >= 1000) return 'Rs.${(n/1000).toStringAsFixed(1)}K'; + return 'Rs.${n.toStringAsFixed(0)}'; + } + String _initials(String name) { + final p = name.trim().split(' '); + return p.length >= 2 ? '${p[0][0]}${p[1][0]}'.toUpperCase() : name.substring(0, name.length.clamp(0, 2)).toUpperCase(); + } + + @override + Widget build(BuildContext context) { + if (!_isAgentRole) { + return MainLayout( + title: 'Partner Dashboard', + body: const SizedBox.shrink(), + ); + } + + return MainLayout( + title: 'Partner Dashboard', + body: SafeArea( + child: loading + ? const Center(child: CircularProgressIndicator(color: Color(0xFF0ABFA3))) + : SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _label('Partner Details'), + _PartnerProfileCard(details: partnerDetails, initials: _initials(partnerDetails['agent_name']?.toString() ?? 'PD')), + const SizedBox(height: 14), + _label('Overview'), + _OverviewBar(details: partnerDetails, inr: _inr), + const SizedBox(height: 14), + // _label('Policy List & Renewal Alerts'), + _PolicyRenewalRow( + policies: allPolicies, + currentPageRenewals: _pageRenewals, totalRenewalPages: _totalPages, + renewalPage: renewalPage, selectedRenewalDays: selectedRenewalDays, + filteredRenewalsCount: filteredRenewals.length, + onRenewalDays: (d) { + final id = _agentId ?? ref.read(userIdProvider); + if (id != null) { + _fetchRenewals(id, days: d); + } else { + _applyRenewalFilter(d); + } + }, + onRenewalPageChanged: (p) => setState(() => renewalPage = p), + inr: _inr, + ), + const SizedBox(height: 14), + _label('Earning Details'), + _EarningDetailsSection( + allFinYears: _allFinYears(), selectedFinYear: selectedFY, + onFinYearChanged: (fy) => setState(() => selectedFY = fy), + aggregate: _fyAgg, earningsForFY: _fyEarnings, inr: _inr, + ), + const SizedBox(height: 20), + ]), + ), + ), + ); + } + + Widget _label(String text) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row(children: [ + Text(text.toUpperCase(), style: GoogleFonts.inter(fontSize: 10.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: .8)), + const SizedBox(width: 10), + const Expanded(child: Divider(color: Color(0xFFD9F0ED), thickness: 1)), + ]), + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// PARTNER PROFILE CARD +// Fields: agent_name, agent_code, mobile, email, manager_name, status +// All come from partnerDetails() → partner_agent table +// ───────────────────────────────────────────────────────────────────────────── +class _PartnerProfileCard extends StatelessWidget { + final Map details; final String initials; + const _PartnerProfileCard({required this.details, required this.initials}); + @override + Widget build(BuildContext context) { + final name = details['agent_name'] ?? '--'; + final code = details['agent_code'] ?? '--'; + final mobile = details['mobile'] ?? '--'; + final email = details['email'] ?? '--'; + final manager = details['manager_name'] ?? '--'; + final status = details['status'] ?? 'Active'; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12)), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Container(width: 48, height: 48, + decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xFF0ABFA3), border: Border.all(color: const Color(0x19000000), width: 2)), + alignment: Alignment.center, + child: Text(initials, style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white))), + const SizedBox(width: 14), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(name.toString(), style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))), + const SizedBox(height: 5), + Wrap(spacing: 16, runSpacing: 4, children: [ + _m('Code', code.toString()), + _m('Mobile', mobile.toString()), + _m('Email', email.toString()), + _m('Manager', manager.toString()), + ]), + ])), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration(color: const Color(0x380ABFA3), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0x4D0ABFA3))), + child: Text('• $status', style: GoogleFonts.inter(fontSize: 11, fontWeight: FontWeight.w600, color: const Color(0xFF0B8D77)))), + ]), + ); + } + Widget _m(String l, String v) => RichText(text: TextSpan( + text: '$l: ', style: GoogleFonts.inter(fontSize: 11.5, color: const Color(0xFF8AABB5)), + children: [TextSpan(text: v, style: GoogleFonts.inter(fontSize: 11.5, fontWeight: FontWeight.w500, color: const Color(0xFF0F2D3D)))])); +} + +// ───────────────────────────────────────────────────────────────────────────── +// OVERVIEW BAR +// mapped_policies / active_policies → COUNT from partner_policy WHERE agent_id=$id +// total_premium / commission_earned → SUM from partner_policy +// enquiry_* → COUNT from partner_enquiry WHERE agent_id=$id, is_active=1 +// endorsement_* → COUNT from partner_endorsement_request WHERE agent_id=$id, is_active=1 +// Clients removed. Mapped Policies shows Active dot only. +// ───────────────────────────────────────────────────────────────────────────── +class _OverviewBar extends StatelessWidget { + final Map details; final String Function(dynamic) inr; + const _OverviewBar({required this.details, required this.inr}); + @override + Widget build(BuildContext context) { + final mp = details['mapped_policies'] ?? '0'; + final ip = details['issued_policies'] ?? '0'; // policy_number IS NOT NULL + final pp2 = details['pending_policies'] ?? '0'; // policy_number IS NULL + final tp = details['total_premium'] ?? '0'; + final com = details['commission_earned'] ?? '0'; + final cr = details['commission_rate'] ?? '15'; + final eq = details['enquiry_total'] ?? '0'; + final eqd = details['enquiry_completed'] ?? '0'; + final eqp = details['enquiry_pending'] ?? '0'; + final en = details['endorsement_total'] ?? '0'; + final end = details['endorsement_done'] ?? '0'; + final enp = details['endorsement_pending'] ?? '0'; + + return Container( + decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(10), + boxShadow: const [BoxShadow(color: Color(0x06000000), blurRadius: 4, offset: Offset(0, 2))]), + child: LayoutBuilder(builder: (ctx, box) { + final wide = box.maxWidth > 700; + if (wide) return IntrinsicHeight(child: Row(children: [ + _sc('Mapped Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')], false), + _sc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY ${_currentFinYear()}')]), + _sc('Delegation', inr(com), const Color(0xFFF97316), [_dot(Colors.grey, '$cr% rate')]), + _fc('Enquiry', eqd.toString(), eq.toString(), const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done'), _dot(const Color(0xFFF97316), '$eqp Pending')]), + _fc('Endorsement', end.toString(), en.toString(), const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done'), _dot(const Color(0xFFF97316), '$enp Pending')]), + ])); + return Wrap(children: [ + _wc('Mapped Policies', mp.toString(), const Color(0xFF0ABFA3), [_dot(const Color(0xFF10B981), '$ip Issued'), _dot(const Color(0xFFF97316), '$pp2 Pending')]), + _wc('Total Premium', inr(tp), const Color(0xFF10B981), [_dot(Colors.grey, 'FY ${_currentFinYear()}')]), + _wc('Delegation', inr(com), const Color(0xFFF97316), [_dot(Colors.grey, '$cr% rate')]), + _wc('Enquiry', '$eqd / $eq', const Color(0xFF3B82F6), [_dot(const Color(0xFF10B981), '$eqd Done')]), + _wc('Endorsement', '$end / $en', const Color(0xFF8B5CF6), [_dot(const Color(0xFF10B981), '$end Done')]), + ]); + }), + ); + } + Widget _sc(String l, String v, Color c, List d, [bool lb=true]) => Expanded(child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: lb ? const BoxDecoration(border: Border(left: BorderSide(color: Color(0xFFD9F0ED)))) : null, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), Text(v,style:_bigNum.copyWith(color:c)), const SizedBox(height:4), Wrap(spacing:8,runSpacing:2,children:d)]))); + Widget _fc(String l, String dn, String tot, Color c, List d) => Expanded(child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: const BoxDecoration(border: Border(left: BorderSide(color: Color(0xFFD9F0ED)))), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), + Row(crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [Text(dn,style:_bigNum.copyWith(color:c)), Text(' / ',style:GoogleFonts.inter(fontSize:14,fontWeight:FontWeight.w300,color:const Color(0xFFCBD5E1))), Text(tot,style:GoogleFonts.inter(fontSize:14,fontWeight:FontWeight.w600,color:const Color(0xFF94A3B8)))]), + const SizedBox(height:4), Wrap(spacing:8,runSpacing:2,children:d)]))); + Widget _wc(String l, String v, Color c, List d) => SizedBox(width:160, child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFD9F0ED)))), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(l,style:_labelSmall), const SizedBox(height:4), Text(v,style:_bigNum.copyWith(color:c)), const SizedBox(height:4), Wrap(spacing:6,children:d)]))); + Widget _dot(Color c, String l) => Row(mainAxisSize: MainAxisSize.min, children: [Container(width:7,height:7,decoration:BoxDecoration(shape:BoxShape.circle,color:c)), const SizedBox(width:4), Text(l,style:_subInfo)]); +} + +// ───────────────────────────────────────────────────────────────────────────── +// POLICY LIST + RENEWAL ALERTS ROW +// ───────────────────────────────────────────────────────────────────────────── +class _PolicyRenewalRow extends StatelessWidget { + final List> policies, currentPageRenewals; + final int totalRenewalPages, renewalPage, selectedRenewalDays, filteredRenewalsCount; + final ValueChanged onRenewalDays, onRenewalPageChanged; + final String Function(dynamic) inr; + const _PolicyRenewalRow({required this.policies, required this.currentPageRenewals, + required this.totalRenewalPages, required this.renewalPage, + required this.selectedRenewalDays, required this.filteredRenewalsCount, + required this.onRenewalDays, required this.onRenewalPageChanged, required this.inr}); + @override + Widget build(BuildContext context) => _RenewalAlertsCard( + currentPageRenewals: currentPageRenewals, + totalPages: totalRenewalPages, + currentPage: renewalPage, + selectedDays: selectedRenewalDays, + totalCount: filteredRenewalsCount, + onDays: onRenewalDays, + onPageChanged: onRenewalPageChanged, + inr: inr, + ); +} + +// ── Policy list card +// holder_name = partner_policy.insured_name (aliased in API) +// product = partner_policy.product column; fallback to vehicle_type +class _PolicyListCard extends StatelessWidget { + final List> policies; final String Function(dynamic) inr; + const _PolicyListCard({required this.policies, required this.inr}); + @override + Widget build(BuildContext context) => _DashCard( + header: Row(children: [Text('Policy List', style: _chartHeader), const SizedBox(width:8), _CountBadge(policies.length)]), + body: policies.isEmpty + ? const _EmptyState(message:'No policies found for this partner') + : Column(children: [ + _TableHeader(headers: const ['Policy No.', 'Holder', 'Product', 'Premium']), + ConstrainedBox(constraints: const BoxConstraints(maxHeight: 340), + child: ListView.separated( + shrinkWrap: true, itemCount: policies.length, + separatorBuilder: (_,__) => const Divider(height:1, color:Color(0xFFD9F0ED)), + itemBuilder: (_,i) { + final p = policies[i]; + final productLabel = (p['product']?.toString().isNotEmpty == true) + ? p['product'].toString() + : (p['vehicle_type']?.toString() ?? '--'); + return Container( + padding: const EdgeInsets.symmetric(vertical:8, horizontal:12), + child: Row(children: [ + Expanded(flex:2, child: Text(p['policy_no']?.toString() ?? '--', style:_tableTeal, overflow:TextOverflow.ellipsis)), + Expanded(flex:2, child: Text(p['holder_name']?.toString() ?? '--', style:_tableData, overflow:TextOverflow.ellipsis)), + Expanded( + flex: 2, + child: Text( + productLabel, + style: _tableData, + overflow: TextOverflow.ellipsis, + ), + ), + Expanded(flex:2, child: Text(inr(p['premium'] ?? 0), style:_tableDataBold)), + ]), + ); + }, + )), + ]), + ); +} + +class _RenewalAlertsCard extends StatelessWidget { + final List> currentPageRenewals; + final int totalPages, currentPage, selectedDays, totalCount; + final ValueChanged onDays, onPageChanged; + final String Function(dynamic) inr; + const _RenewalAlertsCard({required this.currentPageRenewals, required this.totalPages, required this.currentPage, required this.selectedDays, required this.totalCount, required this.onDays, required this.onPageChanged, required this.inr}); + @override + Widget build(BuildContext context) => _DashCard( + header: Row(children: [ + Container(width:30,height:30,decoration:BoxDecoration(color:const Color(0xFFFFEDED),borderRadius:BorderRadius.circular(8)),child:const Icon(Icons.notifications_active_outlined,size:16,color:Color(0xFFEF4444))), + const SizedBox(width:8), + Column(crossAxisAlignment:CrossAxisAlignment.start, children:[Text('Renewal Alerts',style:_chartHeader), Text('At-risk policies',style:GoogleFonts.inter(fontSize:9.5,color:const Color(0xFF8AABB5)))]), + const Spacer(), + Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: const Color(0xFFF2F4F6), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Wrap( + spacing: 2, + runSpacing: 2, + children: [10, 20, 30, 40, 50].map((d) { + final act = selectedDays == d; + return GestureDetector( + onTap: () => onDays(d), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: act ? const Color(0xFF000666) : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: act ? const Color(0xFF000666) : Colors.transparent, + ), + ), + child: Text( + '$d Days', + style: GoogleFonts.inter( + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: .4, + color: act ? Colors.white : const Color(0xFF454652), + ), + ), + ), + ); + }).toList(), + ), + ), + ]), + body: totalCount == 0 + ? const _EmptyState(message:'No renewals due within selected period') + : Padding( + padding: const EdgeInsets.all(10), + child: LayoutBuilder( + builder: (context, constraints) { + final w = constraints.maxWidth; + final crossAxisCount = w >= 1100 ? 4 : w >= 850 ? 3 : w >= 560 ? 2 : 1; + return Column(children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + mainAxisExtent: 90, + ), + itemCount: currentPageRenewals.length, + itemBuilder: (_, i) => _RenewalAlertCard(renewal: currentPageRenewals[i], inr: inr), + ), + if (totalPages > 1) ...[ + const SizedBox(height: 10), + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Text('${currentPage+1} / $totalPages', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF8AABB5))), + const SizedBox(width: 8), + _PA(icon: Icons.chevron_left, enabled: currentPage > 0, onTap: () => onPageChanged(currentPage - 1)), + const SizedBox(width: 4), + _PA(icon: Icons.chevron_right, enabled: currentPage < totalPages - 1, onTap: () => onPageChanged(currentPage + 1)), + ]), + ], + ]); + }, + ), + ), + ); +} + +// renewal card — holder_name=insured_name, premium=premium_amount (both aliased in API) +class _RenewalAlertCard extends StatelessWidget { + final Map renewal; final String Function(dynamic) inr; + const _RenewalAlertCard({required this.renewal, required this.inr}); + @override + Widget build(BuildContext context) { + final d = int.tryParse(renewal['days_left']?.toString()??'0')??0; + Color dc, bb, bf; + if (d<=10){dc=const Color(0xFFEF4444);bb=const Color(0xFFFFEDED);bf=const Color(0xFFEF4444);} + else if(d<=20){dc=const Color(0xFFF97316);bb=const Color(0xFFFEF0E7);bf=const Color(0xFFF97316);} + else{dc=const Color(0xFF94A3B8);bb=const Color(0xFFF1F5F9);bf=const Color(0xFF64748B);} + return Container( + padding:const EdgeInsets.symmetric(horizontal:10,vertical:7), + decoration:BoxDecoration(color:Colors.white,border:Border.all(color:const Color(0xFFE2E8F0)),borderRadius:BorderRadius.circular(10), + boxShadow:const[BoxShadow(color:Color(0x06000000),blurRadius:4,offset:Offset(0,2))]), + child:Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment:CrossAxisAlignment.start,children:[ + Row(children:[Container(width:8,height:8,decoration:BoxDecoration(shape:BoxShape.circle,color:dc)),const Spacer(), + Container(padding:const EdgeInsets.symmetric(horizontal:7,vertical:2),decoration:BoxDecoration(color:bb,borderRadius:BorderRadius.circular(4)), + child:Text('DUE IN $d DAYS',style:GoogleFonts.inter(fontSize:9,fontWeight:FontWeight.w700,color:bf,letterSpacing:.3)))]), + const SizedBox(height:4), + Text(renewal['holder_name']?.toString()??'--',style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF0F2D3D)),maxLines:1,overflow:TextOverflow.ellipsis), + const SizedBox(height:1), + Row(mainAxisAlignment:MainAxisAlignment.spaceBetween,children:[ + Flexible(child:Text('Policy: ${renewal['policy_no']?.toString()??'--'}',style:GoogleFonts.inter(fontSize:9.5,color:const Color(0xFF8AABB5)),overflow:TextOverflow.ellipsis)), + Text(inr(renewal['premium']??0),style:GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w700,color:const Color(0xFF0F2D3D))), + ]), + ])); + } +} + +typedef _PA = _PaginationArrow; +class _PaginationArrow extends StatelessWidget { + final IconData icon; final bool enabled; final VoidCallback onTap; + const _PaginationArrow({required this.icon, required this.enabled, required this.onTap}); + @override + Widget build(BuildContext context) => GestureDetector(onTap:enabled?onTap:null,child:Container(width:28,height:28, + decoration:BoxDecoration(color:enabled?const Color(0xFF0F2D3D):const Color(0xFFF1F5F9),borderRadius:BorderRadius.circular(6)), + child:Icon(icon,size:18,color:enabled?Colors.white:const Color(0xFFCBD5E1)))); +} + +// ───────────────────────────────────────────────────────────────────────────── +// EARNING DETAILS SECTION +// ───────────────────────────────────────────────────────────────────────────── +class _EarningDetailsSection extends StatelessWidget { + final List allFinYears; + final String selectedFinYear; + final ValueChanged onFinYearChanged; + final Map aggregate; + final List> earningsForFY; + final String Function(dynamic) inr; + const _EarningDetailsSection({required this.allFinYears, required this.selectedFinYear, required this.onFinYearChanged, required this.aggregate, required this.earningsForFY, required this.inr}); + + Widget _hero() { + final revenue = inr(aggregate['premium'] ?? 0); + final netPayout = inr(aggregate['netPayout'] ?? 0); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 2))]), + child: LayoutBuilder(builder: (ctx, box) { + final wide = box.maxWidth > 500; + final left = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Text('PERFORMANCE DASHBOARD', style: GoogleFonts.inter(fontSize: 9.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: 1.2)), + // const SizedBox(height: 6), + Text('Monthly Revenue', style: GoogleFonts.inter(fontSize: 16, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))), + const SizedBox(height: 14), + Row(crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ + Text(revenue, style: GoogleFonts.manrope(fontSize: 28, fontWeight: FontWeight.w700, color: const Color(0xFF000666))), + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration(color: const Color(0xFFE6FAF7), borderRadius: BorderRadius.circular(6)), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.trending_up, size: 12, color: Color(0xFF0ABFA3)), + const SizedBox(width: 4), + Text('+12.4%', style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w700, color: const Color(0xFF0ABFA3))), + ]), + ), + ]), + const SizedBox(height: 6), + Text('Accumulated earnings for ${_fyFull(selectedFinYear)}.', style: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF8AABB5))), + ]); + final payout = Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + decoration: BoxDecoration(color: const Color(0xFFF8FFFE), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFD9F0ED))), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.savings_outlined, size: 28, color: Color(0xFF0ABFA3)), + const SizedBox(width: 12), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('TOTAL PAYOUT TO DATE', style: GoogleFonts.inter(fontSize: 8.5, fontWeight: FontWeight.w700, color: const Color(0xFF8AABB5), letterSpacing: .8)), + const SizedBox(height: 4), + Text(netPayout, style: GoogleFonts.manrope(fontSize: 20, fontWeight: FontWeight.w700, color: const Color(0xFF000666))), + ]), + ]), + ); + return wide + ? Row(crossAxisAlignment: CrossAxisAlignment.center, children: [Expanded(child: left), const SizedBox(width: 20), payout]) + : Column(crossAxisAlignment: CrossAxisAlignment.start, children: [left, const SizedBox(height: 16), payout]); + }), + ); + } + + Widget _fySection() { + final agg = aggregate; + final stats = [ + _SI('Financial Year', _fyFull(selectedFinYear), Icons.calendar_today_outlined, const Color(0xFF000666)), + _SI('Premium Collected', inr(agg['premium'] ?? 0), Icons.payments_outlined, const Color(0xFF006B5C)), + _SI('Policies', '${agg['policies'] ?? 0}', Icons.policy_outlined, const Color(0xFF1A237E)), + _SI('Delegation (15%)', inr(agg['commission'] ?? 0), Icons.percent_outlined, const Color(0xFF059669)), + _SI('TDS (10%)', inr(agg['tds'] ?? 0), Icons.account_balance_outlined, const Color(0xFFD97706)), + _SI('Net Payout', inr(agg['netPayout'] ?? 0), Icons.savings_outlined, const Color(0xFF059669)), + ]; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(12)), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + const Icon(Icons.currency_rupee_rounded, size: 16, color: Color(0xFF006B5C)), + const SizedBox(width: 6), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Earning Details', style: GoogleFonts.inter(fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))), + Text('${_fyFull(selectedFinYear)} · ${_fyRange(selectedFinYear)}', style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5))), + ]), + const Spacer(), + _FYTabBar(allFinYears: allFinYears, selected: selectedFinYear, onChanged: onFinYearChanged), + ]), + const SizedBox(height: 14), + LayoutBuilder(builder: (ctx, box) { + final cols = box.maxWidth > 700 ? 6 : box.maxWidth > 450 ? 3 : 2; + final w = (box.maxWidth - (cols - 1) * 10) / cols; + return Wrap(spacing: 10, runSpacing: 10, children: stats.map((s) => SizedBox(width: w, child: _StatCard(item: s))).toList()); + }), + ]), + ); + } + + Widget _txnTable() => Container( + decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFD9F0ED)), borderRadius: BorderRadius.circular(10), + boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 2))]), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11), + decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFFD9F0ED)))), + child: Row(children: [ + Text('Recent Transactions', style: GoogleFonts.inter(fontSize: 13, fontWeight: FontWeight.w700, color: const Color(0xFF0F2D3D))), + const SizedBox(width: 8), _CountBadge(earningsForFY.length), const Spacer(), + Text('Showing for ${_fyFull(selectedFinYear)}', style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5))), + ]), + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), color: const Color(0xFFF2F4F6), + child: Row(children: ['Month','Premium','Policies','Delegation','Net Payout'] + .map((h) => Expanded(flex: 2, child: Text(h, style: _tableHeadStyle))).toList()), + ), + earningsForFY.isEmpty + ? const SizedBox( + height: 180, + child: Center( + child: _EmptyState( + message: 'No transactions for this financial year', + ), + ), + ) + : ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 320), + child: ListView.separated( + shrinkWrap: true, physics: const ClampingScrollPhysics(), + itemCount: earningsForFY.length, + separatorBuilder: (_, __) => const Divider(height: 1, color: Color(0xFFD9F0ED)), + itemBuilder: (_, i) { + final e = earningsForFY[i]; + final label = e['month_label']?.toString() ?? e['month_key']?.toString() ?? '--'; + double pre = double.tryParse(e['premium']?.toString() ?? '0') ?? 0; + double com = double.tryParse(e['commission']?.toString() ?? '0') ?? (pre * 0.15); + double tds = double.tryParse(e['tds']?.toString() ?? '0') ?? (com * 0.10); + double net = double.tryParse(e['net_payout']?.toString() ?? '0') ?? (com - tds); + return Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), + child: Row(children: [ + Expanded(flex:2, child: Text(label, style:_tableTeal, overflow:TextOverflow.ellipsis)), + Expanded(flex:2, child: Text(inr(pre), style:_tableDataBold.copyWith(color:const Color(0xFF000666)))), + Expanded(flex:2, child: Text('${e['policies']??0}', style:_tableDataBold)), + Expanded(flex:2, child: Text(inr(com), style:_tableDataBold.copyWith(color:const Color(0xFF059669)))), + Expanded(flex:2, child: Text(inr(net), style:_tableDataBold.copyWith(color:const Color(0xFF059669)))), + ]), + ); + }, + ), + ), + ]), + ); + + @override + Widget build(BuildContext context) => Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + _hero(), const SizedBox(height: 14), _fySection(), const SizedBox(height: 14), _txnTable(), + ]); +} + +typedef _SI = _StatItem; +class _StatItem { final String label, value; final IconData icon; final Color color; const _StatItem(this.label, this.value, this.icon, this.color); } + +class _StatCard extends StatelessWidget { + final _StatItem item; const _StatCard({required this.item}); + @override + Widget build(BuildContext context) => Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: const [BoxShadow(color: Color(0x07000000), blurRadius: 4, offset: Offset(0, 2))]), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Flexible(child: Text(item.label.toUpperCase(), style: GoogleFonts.inter(fontSize: 8.5, fontWeight: FontWeight.w800, color: const Color(0xFF8AABB5), letterSpacing: .6), overflow: TextOverflow.ellipsis)), + Icon(item.icon, size: 16, color: item.color), + ]), + const SizedBox(height: 8), + Text(item.value, style: GoogleFonts.manrope(fontSize: 18, fontWeight: FontWeight.w700, color: item.color)), + ]), + ); +} + +class _FYTabBar extends StatelessWidget { + final List allFinYears; final String selected; final ValueChanged onChanged; + const _FYTabBar({required this.allFinYears, required this.selected, required this.onChanged}); + @override + Widget build(BuildContext context) => Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration(color: const Color(0xFFF2F4F6), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0))), + child: Wrap(spacing: 2, runSpacing: 2, children: allFinYears.map((fy) { + final act = fy == selected; + return GestureDetector(onTap: () => onChanged(fy), child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration(color: act ? const Color(0xFF000666) : Colors.transparent, borderRadius: BorderRadius.circular(6), border: Border.all(color: act ? const Color(0xFF000666) : Colors.transparent)), + child: Text(_fyShort(fy), style: GoogleFonts.inter(fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: .4, color: act ? Colors.white : const Color(0xFF454652))), + )); + }).toList()), + ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// SHARED SMALL WIDGETS +// ───────────────────────────────────────────────────────────────────────────── +class _DashCard extends StatelessWidget { + final Widget header, body; const _DashCard({required this.header, required this.body}); + @override + Widget build(BuildContext context) => Container( + decoration:BoxDecoration(color:Colors.white,border:Border.all(color:const Color(0xFFD9F0ED)),borderRadius:BorderRadius.circular(10), + boxShadow:const[BoxShadow(color:Color(0x08000000),blurRadius:4,offset:Offset(0,2))]), + child:Column(crossAxisAlignment:CrossAxisAlignment.stretch,children:[ + Container(padding:const EdgeInsets.symmetric(horizontal:14,vertical:11),decoration:const BoxDecoration(border:Border(bottom:BorderSide(color:Color(0xFFD9F0ED)))),child:header), + body])); +} +class _TableHeader extends StatelessWidget { + final List headers; const _TableHeader({required this.headers}); + @override + Widget build(BuildContext context) => Container( + padding:const EdgeInsets.symmetric(vertical:8,horizontal:12), color:const Color(0xFFE3F9F8), + child:Row(children:headers.map((h)=>Expanded(flex:2,child:Text(h,style:_tableHeadStyle))).toList())); +} +class _CountBadge extends StatelessWidget { + final int count; const _CountBadge(this.count); + @override + Widget build(BuildContext context) => Container( + padding:const EdgeInsets.symmetric(horizontal:8,vertical:2), + decoration:BoxDecoration(color:const Color(0xFFE6FAF7),borderRadius:BorderRadius.circular(12)), + child:Text('$count',style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF0ABFA3)))); +} +class _EmptyState extends StatelessWidget { + final String message; const _EmptyState({required this.message}); + @override + Widget build(BuildContext context) => Padding(padding:const EdgeInsets.symmetric(vertical:32), + child:Column(children:[Text('No records found',style:GoogleFonts.inter(fontSize:13,fontWeight:FontWeight.w500,color:const Color(0xFF8AABB5))), + const SizedBox(height:3),Text(message,style:GoogleFonts.inter(fontSize:11.5,color:const Color(0xFFB0C8D0)))])); +} +class _ProductBadge extends StatelessWidget { + final String product; const _ProductBadge(this.product); + @override + Widget build(BuildContext context) => Container( + padding:const EdgeInsets.symmetric(horizontal:8,vertical:2), + decoration:BoxDecoration(color:const Color(0xFFE6FAF7),borderRadius:BorderRadius.circular(20)), + child:Text(product,style:GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w600,color:const Color(0xFF089886)),overflow:TextOverflow.ellipsis)); +} + +// ─── Text Styles ───────────────────────────────────────────────────────────── +final _chartHeader = GoogleFonts.inter(fontSize:12,color:Colors.black,fontWeight:FontWeight.w600); +final _labelSmall = GoogleFonts.inter(fontSize:10,fontWeight:FontWeight.w600,color:const Color(0xFF8AABB5),letterSpacing:.7); +final _bigNum = GoogleFonts.inter(fontSize:21,fontWeight:FontWeight.w700,color:const Color(0xFF0ABFA3)); +final _subInfo = GoogleFonts.inter(fontSize:11,fontWeight:FontWeight.w400,color:const Color(0xFF4A6B78)); +final _tableHeadStyle = GoogleFonts.inter(fontSize:10.5,fontWeight:FontWeight.w600,color:const Color(0xFF8AABB5),letterSpacing:.5); +final _tableData = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w400,color:const Color(0xFF1A3340)); +final _tableDataBold = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w600,color:const Color(0xFF1A3340)); +final _tableTeal = GoogleFonts.inter(fontSize:12,fontWeight:FontWeight.w600,color:const Color(0xFF089886)); \ No newline at end of file diff --git a/lib/presentation/screens/handler/enquiryList.dart b/lib/presentation/screens/handler/enquiryList.dart index bb6e263..7c2c23c 100644 --- a/lib/presentation/screens/handler/enquiryList.dart +++ b/lib/presentation/screens/handler/enquiryList.dart @@ -448,8 +448,16 @@ class EnquiryHandlerState extends ConsumerState { if (response['status'] == 'success') { final data = response['data']; - final fromDate = response['from_date'] ?? ''; - final toDate = response['to_date'] ?? ''; + /* + * Some APIs do not return from_date/to_date in the payload. + * Keep UI date fields stable by falling back to the request dates. + */ + final fromDate = (response['from_date'] ?? '').toString().trim().isNotEmpty + ? response['from_date'].toString() + : (fromDt?.toString() ?? ''); + final toDate = (response['to_date'] ?? '').toString().trim().isNotEmpty + ? response['to_date'].toString() + : (toDt?.toString() ?? ''); print('FromDate : $fromDate'); print('ToDate : $toDate'); diff --git a/lib/presentation/screens/login/login_screen.dart b/lib/presentation/screens/login/login_screen.dart index 6f2981c..92830a8 100644 --- a/lib/presentation/screens/login/login_screen.dart +++ b/lib/presentation/screens/login/login_screen.dart @@ -220,7 +220,9 @@ class _LoginScreenState extends ConsumerState { if (role == 'staff') { context.go(AppRoutes.enquiryForStaff); } else if (role == 'Accounts') { - context.go(AppRoutes.invoiceList); + context.go(AppRoutes.payoutList); + } else if (role == 'agent') { + context.go(AppRoutes.partnerPortalDashboard); } else { context.go(AppRoutes.dashboard); } @@ -482,7 +484,7 @@ class _LoginScreenState extends ConsumerState { ToastHelper.showSuccessToast(context, 'OTP verified successfully'); - await AuthService.saveToken(token); + await AuthService.saveToken(token); await _saveUserRole(token); await _saveIdsFromToken(token); @@ -491,7 +493,9 @@ class _LoginScreenState extends ConsumerState { if (userRole == 'staff') { context.go(AppRoutes.enquiryForStaff); } else if (userRole == 'Accounts') { - context.go(AppRoutes.invoiceList); + context.go(AppRoutes.payoutList); + } else if (userRole == 'agent') { + context.go(AppRoutes.partnerPortalDashboard); } else { context.go(AppRoutes.dashboard); } @@ -754,36 +758,48 @@ class _LoginScreenState extends ConsumerState { ), const SizedBox(height: 40), Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Welcome Back", - style: GoogleFonts.poppins( - fontSize: 50, - fontWeight: FontWeight.w600, - color: Color(0xFF425C5C), + child: LayoutBuilder( + builder: (context, leftPaneConstraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: leftPaneConstraints.maxHeight, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Welcome Back", + style: GoogleFonts.poppins( + fontSize: 50, + fontWeight: FontWeight.w600, + color: Color(0xFF425C5C), + ), + ), + const SizedBox(height: 20), + Image.asset( + "assets/login/login-content.png", + // height: + // MediaQuery.of(context).size.height * + // 0.5, // 40% of screen height + height: leftPaneConstraints.maxHeight * 0.55, + fit: BoxFit.contain, + ), + const SizedBox(height: 10), + Text( + '${'"'}Manage your policies, track quotations, and grow your business all in one place.${'"'}', + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 14, + color: Color(0xFF000000), + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), - ), - const SizedBox(height: 20), - Image.asset( - "assets/login/login-content.png", - height: - MediaQuery.of(context).size.height * - 0.5, // 40% of screen height - fit: BoxFit.contain, - ), - const SizedBox(height: 10), - Text( - '${'"'}Manage your policies, track quotations, and grow your business all in one place.${'"'}', - textAlign: TextAlign.center, - style: GoogleFonts.poppins( - fontSize: 14, - color: Color(0xFF000000), - fontWeight: FontWeight.w500, - ), - ), - ], + ); + }, ), ), ], diff --git a/lib/presentation/screens/payout/custom_dateRange.dart b/lib/presentation/screens/payout/custom_dateRange.dart index 5c39974..5715d26 100644 --- a/lib/presentation/screens/payout/custom_dateRange.dart +++ b/lib/presentation/screens/payout/custom_dateRange.dart @@ -12,6 +12,7 @@ import '../../themes/indicators/date_field_theme.dart'; import '../../themes/indicators/input_field_decoration.dart'; class DateFilterRowPayout extends ConsumerStatefulWidget { + final bool showBrokerFilter; final TextEditingController startController; final String? selectedBroker; final List? selectedParnter; @@ -29,6 +30,7 @@ class DateFilterRowPayout extends ConsumerStatefulWidget { const DateFilterRowPayout({ super.key, + this.showBrokerFilter = true, required this.startController, required this.endController, required this.onFilter, @@ -93,14 +95,23 @@ class _DateFilterRowState extends ConsumerState { // getPartnerDetails(userID); // } - if (managerId != null && widget.selectedBroker != null) { + if (widget.showBrokerFilter && + managerId != null && + widget.selectedBroker != null) { print('managerId - $managerId'); print('SelectedBroker - $widget.selectedBroker'); - getAgentList(managerId,widget.selectedBroker); + getAgentList(managerId); + } + + if (!widget.showBrokerFilter && managerId != null) { + // Broker filter hidden: load partner list directly by manager. + getAgentList(managerId); } }); - getBroker(); + if (widget.showBrokerFilter) { + getBroker(); + } } Future getBroker() async { @@ -134,7 +145,7 @@ class _DateFilterRowState extends ConsumerState { } } - Future getAgentList(id,broker_id) async { + Future getAgentList(id) async { print('getAgentListData called'); setState(() { @@ -142,7 +153,7 @@ class _DateFilterRowState extends ConsumerState { }); try { - final response = await apiService.fetchAgentUnusedCommissionList(id,broker_id); + final response = await apiService.fetchAgentUnusedCommissionList(id); print('getAgentListData called response'); print('get Agent- ${response['data']}'); if (response['status'] == 'success') { @@ -218,8 +229,10 @@ class _DateFilterRowState extends ConsumerState { SizedBox(width: spacing), buildEndDate(context), SizedBox(width: spacing), - buildBroker(context), - SizedBox(width: spacing), + if (widget.showBrokerFilter) ...[ + buildBroker(context), + SizedBox(width: spacing), + ], buildPartner(context), SizedBox(width: spacing), ]; @@ -531,7 +544,7 @@ class _DateFilterRowState extends ConsumerState { }); // 3. Fetch new agents based on this broker - getAgentList(managerId!, selectedId); + getAgentList(managerId!); } }, @@ -548,7 +561,7 @@ class _DateFilterRowState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Referer", style: _textStyle), + Text("Partner", style: _textStyle), SizedBox(height: 5), SizedBox( height: 35, @@ -599,7 +612,7 @@ class _DateFilterRowState extends ConsumerState { decoratorProps: DropDownDecoratorProps( decoration: AppInputDecorations.dropdownDecoration( - label: "Select Referer", + label: "Select Partner", ).copyWith( hintStyle: GoogleFonts.inter( fontSize: 12, @@ -640,7 +653,7 @@ class _DateFilterRowState extends ConsumerState { decoration: InputDecoration( filled: true, fillColor: Colors.white, - hintText: "Search Referer...", + hintText: "Search Partner...", hintStyle: GoogleFonts.inter( fontSize: 11, color: Colors.black, diff --git a/lib/presentation/screens/payout/invoice_list.dart b/lib/presentation/screens/payout/invoice_list.dart index 46349af..010474e 100644 --- a/lib/presentation/screens/payout/invoice_list.dart +++ b/lib/presentation/screens/payout/invoice_list.dart @@ -38,6 +38,11 @@ class _InvoiceListState extends ConsumerState { List tabHeader = ['startDate', 'endDate']; dynamic SelectedStatus; dynamic SelectedStaffId; + DateTime _paymentDate = DateTime.now(); + dynamic _hoveredHistoryInvoiceId; + static const double _actionIconSize = 12; + static const double _actionButtonSize = 28; + static const double _actionIconGap = 2; @override void initState() { @@ -186,6 +191,21 @@ class _InvoiceListState extends ConsumerState { (item['pos_name'] ?? '-').toLowerCase().contains( query.toLowerCase(), ) || + (item['partner_names'] ?? '-').toLowerCase().contains( + query.toLowerCase(), + ) || + (item['utr_numbers'] ?? '-').toLowerCase().contains( + query.toLowerCase(), + ) || + (item['invoiced_amount'] ?? '0').toLowerCase().contains( + query.toLowerCase(), + ) || + (item['payout_amount'] ?? '0').toLowerCase().contains( + query.toLowerCase(), + ) || + (item['balance_amount'] ?? '0').toLowerCase().contains( + query.toLowerCase(), + ) || (item['payout_status'] ?? '-').toLowerCase().contains( query.toLowerCase(), ) @@ -198,15 +218,6 @@ class _InvoiceListState extends ConsumerState { final TextEditingController _searchStaffController = TextEditingController(); - String _formatDateTime(String rawDate) { - try { - final dateTime = DateTime.parse(rawDate); - return DateFormat('dd-MM-yyyy HH:mm').format(dateTime); // 24-hour format - } catch (e) { - return rawDate; // fallback if parsing fails - } - } - String _formatDate(String rawDate) { try { final dateTime = DateTime.parse(rawDate); @@ -216,6 +227,362 @@ class _InvoiceListState extends ConsumerState { } } + String _formatTime(String rawDate) { + try { + final dateTime = DateTime.parse(rawDate); + return DateFormat('HH:mm').format(dateTime); + } catch (e) { + return '-'; + } + } + + String _displayText(dynamic value) { + if (value == null) return '-'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '-'; + return text; + } + + String _truncateText(String value, {int maxChars = 25}) { + if (value.length <= maxChars) return value; + return '${value.substring(0, maxChars)}...'; + } + + String _partnerTooltipText(dynamic value) { + final text = _displayText(value); + if (text == '-') return text; + final items = text + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + if (items.isEmpty) return text; + return items.join('\n'); + } + + String _utrTooltipText(dynamic value) { + final text = _displayText(value); + if (text == '-') return text; + final items = text + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + if (items.isEmpty) return text; + return items.join('\n'); + } + + String _displayAmount(dynamic value) { + if (value == null) return '0'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '0'; + return text; + } + + double _toDouble(dynamic value) { + if (value == null) return 0; + return double.tryParse(value.toString()) ?? 0; + } + + String _generateUtrNumber() { + final now = DateTime.now(); + return 'UTR${DateFormat('yyyyMMddHHmmss').format(now)}'; + } + + Future _showAddPaymentDialog(Map item) async { + final TextEditingController amountController = TextEditingController(); + _paymentDate = DateTime.now(); + dynamic selectedUtrId; + List> utrDetails = []; + String generatedUtrNo = _generateUtrNumber(); + double totalAmount = _toDouble(item['invoiced_amount']); + double paidToDate = _toDouble(item['payout_amount']); + double currentBalance = _toDouble(item['balance_amount']); + double selectedUtrExistingPaid = 0; + double maxAllowedForSelectedUtr = 0; + + try { + final utrResponse = await apiService.getInvoiceUtrDetails(item['id']); + if (utrResponse['status'] == 'success') { + final dataRoot = utrResponse['data'] is Map + ? Map.from(utrResponse['data']) + : {}; + final invoiceSummary = + dataRoot['invoice'] is Map + ? Map.from(dataRoot['invoice']) + : {}; + totalAmount = _toDouble(invoiceSummary['total_amount']); + paidToDate = _toDouble(invoiceSummary['paid_to_date']); + currentBalance = _toDouble(invoiceSummary['balance_amount']); + utrDetails = List>.from(dataRoot['utrs_details'] ?? []); + if (utrDetails.isEmpty) { + generatedUtrNo = _generateUtrNumber(); + } + } + } catch (e) { + ToastHelper.showWarningToast( + context, + 'Failed to load UTR list. Please refresh and try again.', + ); + print('UTR load error: $e'); + } + + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + return StatefulBuilder( + builder: (ctx, setLocalState) { + return AlertDialog( + title: Text( + 'Add Paid Amount', + style: GoogleFonts.poppins(fontSize: 16, fontWeight: FontWeight.w600), + ), + content: SizedBox( + width: 360, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Invoice number #${_displayText(item['invoice_no'])}', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Text( + 'Total Amount', + style: GoogleFonts.inter(fontSize: 12), + ), + ), + Text(': ${totalAmount.toStringAsFixed(2)}', + style: GoogleFonts.inter(fontSize: 12)), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: Text( + 'Amount Paid Till Date', + style: GoogleFonts.inter(fontSize: 12), + ), + ), + Text(': ${paidToDate.toStringAsFixed(2)}', + style: GoogleFonts.inter(fontSize: 12)), + ], + ), + const SizedBox(height: 8), + const Divider(thickness: 1), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + 'Balance Amount', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + Text( + ': ${currentBalance.toStringAsFixed(2)}', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + if (currentBalance > 0) ...[ + if (utrDetails.isNotEmpty) + DropdownButtonFormField( + initialValue: selectedUtrId, + isExpanded: true, + decoration: const InputDecoration( + labelText: 'UTR Number', + border: OutlineInputBorder(), + ), + items: utrDetails.map((utr) { + return DropdownMenuItem( + value: utr['id'], + child: Text(_displayText(utr['utr_no'])), + ); + }).toList(), + onChanged: (value) { + final selected = utrDetails.firstWhere( + (row) => '${row['id']}' == '$value', + orElse: () => {}, + ); + final selectedPaid = _toDouble(selected['paid_amount']); + final selectedDate = selected['utr_date']; + setLocalState(() { + selectedUtrId = value; + selectedUtrExistingPaid = selectedPaid; + maxAllowedForSelectedUtr = + currentBalance + selectedUtrExistingPaid; + amountController.text = selectedPaid > 0 + ? selectedPaid.toStringAsFixed(2) + : ''; + if (selectedDate != null && + selectedDate.toString().trim().isNotEmpty) { + _paymentDate = + DateTime.tryParse(selectedDate.toString()) ?? + _paymentDate; + } + }); + }, + ) + else + TextFormField( + readOnly: true, + initialValue: generatedUtrNo, + decoration: const InputDecoration( + labelText: 'Generated UTR Number', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: 'Paid Amount', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + readOnly: true, + controller: TextEditingController( + text: DateFormat('dd-MM-yyyy').format(_paymentDate), + ), + decoration: const InputDecoration( + labelText: 'Paid Date', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_today_outlined, size: 18), + ), + onTap: () async { + final now = DateTime.now(); + final minDate = DateTime(now.year, now.month, now.day); + final picked = await showDatePicker( + context: ctx, + initialDate: _paymentDate.isBefore(minDate) + ? minDate + : _paymentDate, + firstDate: minDate, + lastDate: DateTime(2100), + ); + if (picked != null) { + setLocalState(() { + _paymentDate = picked; + }); + } + }, + ), + ] else + Text( + 'Invoice fully paid. No payment entry required.', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: const Color(0xFF2E7D6E), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancel'), + ), + if (currentBalance > 0) + ElevatedButton( + onPressed: () async { + final enteredAmount = _toDouble(amountController.text.trim()); + if (enteredAmount <= 0) { + ToastHelper.showWarningToast(context, 'Enter valid paid amount'); + return; + } + if (utrDetails.isNotEmpty && selectedUtrId == null) { + ToastHelper.showWarningToast(context, 'Please choose the UTR number'); + return; + } + if (enteredAmount < selectedUtrExistingPaid) { + ToastHelper.showWarningToast( + context, + 'Paid amount cannot be less than existing paid amount', + ); + return; + } + final maxAllowed = maxAllowedForSelectedUtr > 0 + ? maxAllowedForSelectedUtr + : currentBalance; + if (enteredAmount > maxAllowed) { + ToastHelper.showWarningToast( + context, + 'Paid amount cannot exceed balance amount', + ); + return; + } + Navigator.pop(ctx, true); + }, + child: const Text('Save'), + ), + ], + ); + }, + ); + }, + ); + + if (result != true) return; + + try { + setState(() => isLoading = true); + final response = await apiService.updateInvoiceUtrDetails({ + 'invoice_id': int.tryParse(item['id'].toString()) ?? 0, + 'paid_amount': _toDouble(amountController.text.trim()), + 'utr_id': selectedUtrId != null ? int.tryParse('$selectedUtrId') : null, + 'utr_no': selectedUtrId == null ? generatedUtrNo : null, + 'paid_date': DateFormat('yyyy-MM-dd').format(_paymentDate), + 'created_by': userId, + 'updated_by': userId, + }); + + if (response['status'] == 'success') { + ToastHelper.showSuccessToast(context, 'UTR details updated successfully'); + await getInvoiceList(); + } else { + ToastHelper.showWarningToast( + context, + response['message']?.toString() ?? 'Failed to update UTR details', + ); + } + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + setState(() => isLoading = false); + } + } + + void _showAccountHistoryModal(Map item) { + showDialog( + context: context, + builder: (_) => AccountHistoryModal( + invoiceId: item['id'], + ), + ); + } + @override Widget build(BuildContext context) { return MainLayout( @@ -373,20 +740,30 @@ class _InvoiceListState extends ConsumerState { data: filteredData, txt: true, displayHeaders: [ + "S.No", "Invoice No", "Invoice Date", - "Invoice Amount", - "Broker ", - "POS ", - "Status ", + "Broker Name", + "POS Name", + "Referer", + "UTR Details", + "Invoiced Amount", + "Payout Amount", + "Balance Amount", + "Status", "Updated Date", ], keys: [ - "invoice_date", + "id", "invoice_no", - "invoice_amount_indian_format", + "invoice_date", "broker_name", "pos_name", + "partner_names", + "utr_numbers", + "invoiced_amount", + "payout_amount", + "balance_amount", "payout_status", "updated_at", ], @@ -403,20 +780,34 @@ class _InvoiceListState extends ConsumerState { padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ - Expanded(flex: 3, child: Text('Invoice No', style: _headerStyle)), + Expanded(flex: 1, child: Text('S.No', style: _headerStyle)), Expanded( flex: 2, child: Text('Invoice Date', style: _headerStyle), ), Expanded( - flex: 2, - child: Text('Invoice Amount', style: _headerStyle), + flex: 3, + child: Text('Invoice No', style: _headerStyle), ), Expanded( flex: 2, child: Text('Broker Name', style: _headerStyle), ), Expanded(flex: 2, child: Text('POS Name', style: _headerStyle)), + Expanded(flex: 3, child: Text('Referer', style: _headerStyle)), + Expanded(flex: 2, child: Text('UTR Details', style: _headerStyle)), + Expanded( + flex: 2, + child: Text('Invoiced\nAmount', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Payout\nAmount', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Balance\nAmount', style: _headerStyle), + ), Expanded(flex: 2, child: Text('Status', style: _headerStyle)), Expanded( flex: 2, @@ -426,7 +817,6 @@ class _InvoiceListState extends ConsumerState { ], ), ), - Expanded(child: _buildDataTable(context)), ], ); @@ -474,8 +864,8 @@ class _InvoiceListState extends ConsumerState { child: Row( children: [ Expanded( - flex: 3, - child: Text(item['invoice_no'] ?? '-', style: _dataBold), + flex: 1, + child: Text('$sno', style: _dataBold), ), Expanded( flex: 2, @@ -487,9 +877,9 @@ class _InvoiceListState extends ConsumerState { ), ), Expanded( - flex: 2, + flex: 3, child: Text( - item['invoice_amount_indian_format'] ?? '-', + _displayText(item['invoice_no']), style: _dataBold, softWrap: true, maxLines: 3, @@ -498,7 +888,7 @@ class _InvoiceListState extends ConsumerState { Expanded( flex: 2, child: Text( - item['broker_name'] != null ? item['broker_name'] : '-', + _displayText(item['broker_name']), style: _dataBold, softWrap: true, maxLines: 3, @@ -507,7 +897,62 @@ class _InvoiceListState extends ConsumerState { Expanded( flex: 2, child: Text( - item['pos_name'] != null ? item['pos_name'] : '-', + _displayText(item['pos_name']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 3, + child: Tooltip( + message: _partnerTooltipText(item['partner_names']), + waitDuration: const Duration(milliseconds: 250), + child: Text( + _truncateText(_displayText(item['partner_names']), maxChars: 25), + style: _dataBold, + softWrap: false, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + Expanded( + flex: 2, + child: Tooltip( + message: _utrTooltipText(item['utr_numbers']), + waitDuration: const Duration(milliseconds: 250), + child: Text( + _truncateText(_displayText(item['utr_numbers']), maxChars: 10), + style: _dataBold, + softWrap: false, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + _displayAmount(item['invoiced_amount']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 2, + child: Text( + _displayAmount(item['payout_amount']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 2, + child: Text( + _displayAmount(item['balance_amount']), style: _dataBold, softWrap: true, maxLines: 3, @@ -530,14 +975,26 @@ class _InvoiceListState extends ConsumerState { Expanded( flex: 2, - child: Text( - item['updated_at'] != null - ? _formatDateTime(item['updated_at']) - : '-', - style: _dataBold, - softWrap: true, - maxLines: 3, - ), + child: item['updated_at'] != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatDate(item['updated_at']), + style: _dataBold, + softWrap: false, + maxLines: 1, + ), + Text( + _formatTime(item['updated_at']), + style: _dataBold, + softWrap: false, + maxLines: 1, + ), + ], + ) + : Text('-', style: _dataBold), ), // ACTION ICONS (Edit + Delete) @@ -546,36 +1003,94 @@ class _InvoiceListState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - // EDIT ICON + // Step 1: Open payout/view screen for selected invoice. IconButton( icon: const Icon( Icons.remove_red_eye_outlined, color: Colors.blue, - size: 18, + size: _actionIconSize, ), + tooltip: 'View invoice', onPressed: () { // TODO: handle edit context.go(AppRoutes.payout, extra: item); print("Edit clicked for ${item['invoice_no']}"); }, - splashRadius: 18, + splashRadius: 16, hoverColor: Colors.black12, - padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, ), + const SizedBox(width: _actionIconGap), - // DELETE ICON + // Step 2: Add payment only when balance amount is pending. IconButton( - icon: const Icon(Icons.delete, color: Colors.red, size: 18), + icon: const Icon( + Icons.currency_rupee, + color: Colors.green, + size: _actionIconSize, + ), + tooltip: 'Add payment', + onPressed: () { + final balance = _toDouble(item['balance_amount']); + if (balance <= 0) { + ToastHelper.showWarningToast(context, 'Invoice already fully paid'); + return; + } + _showAddPaymentDialog(item); + }, + splashRadius: 16, + hoverColor: Colors.black12, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, + ), + const SizedBox(width: _actionIconGap), + + // Step 3: Show account history modal for this invoice. + IconButton( + icon: const Icon( + Icons.history, + color: Colors.green, + size: _actionIconSize, + ), + tooltip: 'Account details', + onPressed: () => _showAccountHistoryModal(item), + splashRadius: 16, + hoverColor: Colors.black12, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, + ), + const SizedBox(width: _actionIconGap), + + // Step 4: Delete invoice entry. + IconButton( + icon: const Icon(Icons.delete, color: Colors.red, size: _actionIconSize), + tooltip: 'Delete invoice', onPressed: () { // TODO: handle delete deleteInvoice(item['id']); print("Delete clicked for ${item['invoice_no']}"); }, - splashRadius: 18, + splashRadius: 16, hoverColor: Colors.black12, - padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, ), ], ), @@ -597,3 +1112,738 @@ class _InvoiceListState extends ConsumerState { color: Color(0xFF1E293B), ); } + +class AccountHistoryModal extends StatefulWidget { + final dynamic invoiceId; + const AccountHistoryModal({super.key, required this.invoiceId}); + + @override + State createState() => _AccountHistoryModalState(); +} + +class _AccountHistoryModalState extends State { + late ApiService apiService; + bool isLoading = true; + List> accountHistoryData = []; + Map? invoiceSummary; + final TextStyle _historyHeaderStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: const Color(0xFF1E293B), + ); + final TextStyle _historyCellStyle = GoogleFonts.inter( + fontSize: 11.5, + fontWeight: FontWeight.w400, + color: const Color(0xFF000000), + ); + + String _formatINR(dynamic value) { + final amount = double.tryParse(value?.toString() ?? '') ?? 0; + return NumberFormat.currency( + locale: 'en_IN', + symbol: '₹', + decimalDigits: 0, + ).format(amount); + } + + String _formatTimelineDate(dynamic rawDate) { + if (rawDate == null) return '-'; + try { + final dt = DateTime.parse(rawDate.toString()); + return DateFormat('d MMM yyyy').format(dt); + } catch (_) { + return rawDate.toString(); + } + } + + double _toDouble(dynamic value) { + if (value == null) return 0; + return double.tryParse(value.toString()) ?? 0; + } + + Widget _buildWebActivityBody() { + // Step 1: Sort payments by paid date so we can take "latest" balance. + final sortedPayments = [...accountHistoryData]..sort((a, b) { + DateTime da = DateTime.tryParse((a['paid_date'] ?? '').toString()) ?? + DateTime.fromMillisecondsSinceEpoch(0); + DateTime db = DateTime.tryParse((b['paid_date'] ?? '').toString()) ?? + DateTime.fromMillisecondsSinceEpoch(0); + return da.compareTo(db); + }); + + final latestBalance = sortedPayments.isEmpty + ? 0.0 + : _toDouble(sortedPayments.last['balance_amount']); + final paidToDate = sortedPayments.fold( + 0.0, + (sum, item) => sum + _toDouble(item['paid_amount']), + ); + final totalAmount = invoiceSummary != null + ? _toDouble(invoiceSummary?['total_amount']) + : paidToDate + latestBalance; + + // Step 2: Build UI sections (responsive and cleaner typography). + final invoiceNo = _displayText(invoiceSummary?['invoice_no']); + final invoiceLabel = + invoiceNo.startsWith('#') ? invoiceNo : '#$invoiceNo'; + final width = MediaQuery.of(context).size.width; + final bool isCompact = width < 1200; + final double titleFont = isCompact ? 17 : 20; + final double amountBigFont = isCompact ? 22 : 26; + final double sectionTitleFont = isCompact ? 12 : 14; + final double bodyFont = isCompact ? 11 : 12; + final double valueFont = isCompact ? 14 : 16; + + return Padding( + padding: EdgeInsets.all(isCompact ? 10 : 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Step 2a: Top row (Invoice ID only). + Row( + children: [ + Text( + invoiceLabel, + style: GoogleFonts.inter( + fontSize: titleFont, + fontWeight: FontWeight.w800, + color: const Color(0xFF0F172A), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Step 2b: Total amount card. + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'TOTAL AMOUNT', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + color: const Color(0xFF334155), + ), + ), + const SizedBox(height: 6), + Text( + _formatINR(totalAmount), + style: GoogleFonts.inter( + fontSize: amountBigFont, + fontWeight: FontWeight.w900, + color: const Color(0xFF0B3A6B), + ), + ), + ], + ), + const Spacer(), + Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: const Color(0xFFEAF2FF), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.receipt_long_rounded, + color: Color(0xFF2563EB), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Step 2c: Payment timeline header. + Row( + children: [ + Text( + 'PAYMENT TIMELINE', + style: GoogleFonts.poppins( + fontSize: sectionTitleFont, + fontWeight: FontWeight.w700, + color: const Color(0xFF111827), + ), + ), + const Spacer(), + Text( + '${sortedPayments.length} TRANSACTIONS', + style: GoogleFonts.inter( + fontSize: bodyFont, + fontWeight: FontWeight.w600, + color: const Color(0xFF334155), + ), + ), + ], + ), + + const SizedBox(height: 10), + + // Step 2d: Timeline list. + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(top: 2), + itemCount: sortedPayments.length, + itemBuilder: (context, index) { + // Show latest first on the UI. + final data = + sortedPayments[sortedPayments.length - 1 - index]; + final name = _displayText(data['createdby_name']); + final paidAmount = _formatINR(data['paid_amount']); + final date = _formatTimelineDate(data['paid_date']); + + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Color(0xFFECFDF5), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check_circle, + size: 22, + color: Color(0xFF16A34A), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: GoogleFonts.inter( + fontSize: isCompact ? 12 : 13, + fontWeight: FontWeight.w700, + color: const Color(0xFF0F172A), + ), + ), + const SizedBox(height: 4), + Text( + date, + style: GoogleFonts.inter( + fontSize: bodyFont, + fontWeight: FontWeight.w500, + color: const Color(0xFF475569), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + paidAmount, + style: GoogleFonts.inter( + fontSize: isCompact ? 14 : 15, + fontWeight: FontWeight.w800, + color: const Color(0xFF0B3A6B), + ), + ), + ], + ), + ], + ), + ); + }, + ), + ), + + const SizedBox(height: 12), + + // Step 2e: Bottom summary. + Row( + children: [ + Expanded( + child: Text( + 'TOTAL AMOUNT', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w700, + color: const Color(0xFF334155), + ), + ), + ), + Text( + _formatINR(totalAmount), + style: GoogleFonts.inter( + fontSize: valueFont, + fontWeight: FontWeight.w900, + color: const Color(0xFF0F172A), + ), + ), + ], + ), + + const SizedBox(height: 8), + + Row( + children: [ + Expanded( + child: Text( + 'PAID TO DATE', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w700, + color: const Color(0xFF334155), + ), + ), + ), + Text( + _formatINR(paidToDate), + style: GoogleFonts.inter( + fontSize: valueFont, + fontWeight: FontWeight.w900, + color: const Color(0xFF0F172A), + ), + ), + ], + ), + + const SizedBox(height: 10), + + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + decoration: BoxDecoration( + color: const Color(0xFFBFDBFE).withOpacity(0.45), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Expanded( + child: Text( + 'BALANCE', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w800, + color: const Color(0xFF0F172A), + ), + ), + ), + Text( + _formatINR(latestBalance), + style: GoogleFonts.inter( + fontSize: isCompact ? 16 : 18, + fontWeight: FontWeight.w900, + color: const Color(0xFF0B3A6B), + ), + ), + ], + ), + ), + ], + ), + ); + } + + @override + void initState() { + super.initState(); + apiService = ApiService(); + fetchAccountHistory(); + } + + String _displayText(dynamic value) { + if (value == null) return '-'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '-'; + return text; + } + + String _displayAmount(dynamic value) { + if (value == null) return '0.00'; + final parsed = double.tryParse(value.toString()) ?? 0; + return parsed.toStringAsFixed(2); + } + + String _formatIndianDate(dynamic rawDate) { + if (rawDate == null) return '-'; + try { + final dateTime = DateTime.parse(rawDate.toString()); + return DateFormat('dd-MM-yyyy hh:mm a').format(dateTime); + } catch (_) { + return rawDate.toString(); + } + } + + DateTime _parseDateTime(dynamic rawDate) { + if (rawDate == null) { + return DateTime.fromMillisecondsSinceEpoch(0); + } + return DateTime.tryParse(rawDate.toString()) ?? + DateTime.fromMillisecondsSinceEpoch(0); + } + + DateTime _rowEffectiveDate(Map row) { + final paidAt = _parseDateTime(row['paid_date']); + if (paidAt.millisecondsSinceEpoch > 0) return paidAt; + final createdAt = _parseDateTime(row['created_at']); + if (createdAt.millisecondsSinceEpoch > 0) return createdAt; + return _parseDateTime(row['updated_at']); + } + + String _formatPaidDateTime(Map row) { + final paidAt = _parseDateTime(row['paid_date']); + if (paidAt.millisecondsSinceEpoch > 0 && paidAt.hour + paidAt.minute > 0) { + return DateFormat('dd-MM-yyyy hh:mm a').format(paidAt); + } + final createdAt = _parseDateTime(row['created_at']); + if (createdAt.millisecondsSinceEpoch > 0) { + return DateFormat('dd-MM-yyyy hh:mm a').format(createdAt); + } + if (paidAt.millisecondsSinceEpoch > 0) { + return DateFormat('dd-MM-yyyy').format(paidAt); + } + return '-'; + } + + List> _buildMeaningfulHistoryRows() { + final sortedRows = [...accountHistoryData]..sort((a, b) { + final dateCompare = _rowEffectiveDate(a).compareTo(_rowEffectiveDate(b)); + if (dateCompare != 0) return dateCompare; + final idA = int.tryParse('${a['id'] ?? 0}') ?? 0; + final idB = int.tryParse('${b['id'] ?? 0}') ?? 0; + return idA.compareTo(idB); + }); + + double runningPaid = 0; + final rowsWithMetrics = sortedRows.map((row) { + final paid = _toDouble(row['paid_amount']); + runningPaid += paid; + return { + ...row, + 'total_paid_till_date': runningPaid, + }; + }).toList(); + + return rowsWithMetrics.reversed.toList(); + } + + Future fetchAccountHistory() async { + setState(() => isLoading = true); + try { + final response = await apiService.listInvoiceUtrDetails(widget.invoiceId); + if (response['status'] == 'success') { + setState(() { + accountHistoryData = List>.from( + response['data'] ?? [], + ); + invoiceSummary = response['invoice'] is Map + ? Map.from(response['invoice']) + : null; + }); + } else { + setState(() { + accountHistoryData = []; + invoiceSummary = null; + }); + } + } catch (_) { + setState(() { + accountHistoryData = []; + invoiceSummary = null; + }); + } finally { + if (mounted) setState(() => isLoading = false); + } + } + + String _safeFileNamePart(String value) { + return value.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim(); + } + + Future _exportAccountHistoryExcel() async { + if (accountHistoryData.isEmpty) { + ToastHelper.showWarningToast(context, 'No account details data to export'); + return; + } + + final invoiceNo = _displayText(invoiceSummary?['invoice_no']); + final cleanedInvoiceNo = _safeFileNamePart( + invoiceNo == '-' ? 'Invoice number' : invoiceNo, + ); + + final exportRows = _buildMeaningfulHistoryRows(); + // ExcelExporter internally reverses incoming rows before writing. + // So we pass oldest->latest to get latest->oldest in the final file. + final exportInputRows = exportRows.reversed.toList(); + final exportData = exportInputRows.asMap().entries.map((entry) { + final index = entry.key; + final row = entry.value; + return { + 's_no': index + 1, + 'invoice_no': _displayText(row['invoice_no']), + 'utr_no': _displayText(row['utr_no']), + 'total_amount': _displayAmount(row['total_amount']), + 'paid_date_time': _formatPaidDateTime(row), + 'createdby_name': _displayText(row['createdby_name']), + 'paid_amount': _displayAmount(row['paid_amount']), + 'total_paid_till_date': _displayAmount(row['total_paid_till_date']), + 'balance_amount': _displayAmount(row['balance_amount']), + }; + }).toList(); + + await ExcelExporter.exportToExcel( + sheetName: 'Account Details', + fileName: 'Account details $cleanedInvoiceNo', + data: exportData, + displayHeaders: const [ + 'S.No', + 'Invoice Number', + 'UTR Number', + 'Total Amount', + 'Paid Date & Time', + 'Updated By', + 'Paid Amount', + 'Total Paid Till Date', + 'Balance', + ], + keys: const [ + 's_no', + 'invoice_no', + 'utr_no', + 'total_amount', + 'paid_date_time', + 'createdby_name', + 'paid_amount', + 'total_paid_till_date', + 'balance_amount', + ], + ); + } + + @override + Widget build(BuildContext context) { + final bool isMobile = ResponsiveLayout.isMobile(context); + final media = MediaQuery.of(context).size; + + return Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 24, + vertical: isMobile ? 18 : 24, + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isMobile ? media.width : 1100, + maxHeight: media.height * (isMobile ? 0.86 : 0.82), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFFF8FAFC), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), + ), + border: Border(bottom: BorderSide(color: Color(0xFFE5E7EB))), + ), + child: Row( + children: [ + Text( + 'Account Details', + style: GoogleFonts.poppins( + fontSize: 14, + color: const Color(0xFF2E7D6E), + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: isLoading ? null : _exportAccountHistoryExcel, + icon: const Icon(Icons.file_download_outlined, size: 18), + label: const Text('Export Excel'), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF2E7D6E), + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon( + Icons.close, + size: 18, + color: Color(0xFF2E7D6E), + ), + splashRadius: 18, + ), + ], + ), + ), + Expanded( + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : accountHistoryData.isEmpty + ? const Center(child: Text('No Account Details Data Found')) + : Padding( + padding: const EdgeInsets.all(12), + child: Container( + color: Colors.white, + child: LayoutBuilder( + builder: (context, constraints) { + final historyRows = _buildMeaningfulHistoryRows(); + return Scrollbar( + thumbVisibility: true, + child: SingleChildScrollView( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.maxWidth, + ), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + const Color(0xFFF1F5F9), + ), + dataRowMinHeight: 42, + dataRowMaxHeight: 52, + columnSpacing: isMobile ? 14 : 20, + columns: [ + DataColumn( + label: Text('S.No', style: _historyHeaderStyle), + ), + DataColumn( + label: Text( + 'Invoice Number', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'Total Amount', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'UTR Number', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'Paid Date & Time', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text('Updated By', style: _historyHeaderStyle), + ), + DataColumn( + label: Text( + 'Paid Amount', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'Total Paid Till Date', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text('Balance', style: _historyHeaderStyle), + ), + ], + rows: List.generate(historyRows.length, (index) { + final data = historyRows[index]; + return DataRow( + cells: [ + DataCell( + Text('${index + 1}', style: _historyCellStyle), + ), + DataCell( + Text( + _displayText(data['invoice_no']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['total_amount']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayText(data['utr_no']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _formatPaidDateTime(data), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayText(data['createdby_name']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['paid_amount']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['total_paid_till_date']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['balance_amount']), + style: _historyCellStyle, + ), + ), + ], + ); + }), + ), + ), + ), + ), + ); + }, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/screens/payout/payout_details.dart b/lib/presentation/screens/payout/payout_details.dart new file mode 100644 index 0000000..d5480bf --- /dev/null +++ b/lib/presentation/screens/payout/payout_details.dart @@ -0,0 +1,1962 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:excel/excel.dart' hide Border; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:intl/intl.dart'; +import 'package:nhance_partner/data/utils/toastNotification.dart'; + +import '../../../core/routing/routes.dart'; +import '../../../core/services/api_service.dart'; +import '../../layouts/main_layout.dart'; +import '../../layouts/responsive_layout.dart'; +import '../../providers/manager_provider.dart'; +import '../../providers/userRoleProvider.dart'; +import '../../themes/indicators/input_field_decoration.dart'; +import '../../themes/indicators/search_field_theme.dart'; +import 'FormFieldBox.dart'; +import 'custom_dateRange.dart'; + +class PayOutDetails extends ConsumerStatefulWidget { + final Map? editItem; + + const PayOutDetails({super.key, this.editItem}); + + @override + ConsumerState createState() => _PayOutDetailsState(); +} + +class _PayOutDetailsState extends ConsumerState { + static final List _commissionInputFormatter = [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), + ]; + static final List _utrInputFormatter = [ + FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')), + ]; + // -------------------------- + // Invoice Fields + // -------------------------- + final _formKey = GlobalKey(); + // final TextEditingController invoiceNoController; + DateTime invoiceDate = DateTime.now(); + // dynamic selectedAgentId; + List? selectedAgentId; + dynamic selectedBrokerID; + DateTime? policyTillDate = DateTime.now(); + + dynamic totalPolicies = '0'; + dynamic totalCommission = '0'; + bool isEdit = false; + + List> masterPolicies = []; + + final TextEditingController _searchStaffController = TextEditingController(); + + // -------------------------- + // Filters + // -------------------------- + DateTime? fromDate; + DateTime? toDate; + + Map controllers = {}; + List tabHeader = [ + 'startDate', + 'endDate', + 'invoiceNo', + 'invoiceDate', + 'invoiceStatus', + ]; + + // -------------------------- + // Collapse States + // -------------------------- + bool invoiceCollapsed = false; + bool policyCollapsed = false; + + // -------------------------- + // Sample Data (replace with API later) + // -------------------------- + final List> allPolicies = []; + + List> filteredPolicies = []; + Set selectedPolicies = {}; + + List> getBrokerData = []; + List> filteredBrokerData = []; + + List> getAgentData = []; + List> filteredAgentData = []; + + bool isLoading = false; + bool isLoadingEditData = false; + bool hasFetchedTableData = false; + final Map _uploadedUtrByPolicyKey = {}; + final Set _inlineSavingIds = {}; + late ApiService apiService; + dynamic managerId; + dynamic userId; + dynamic roleId; + final DateFormat df = DateFormat('dd-MM-yyyy'); + + @override + void initState() { + super.initState(); + apiService = ApiService(); + + for (String field in tabHeader) { + controllers[field] = TextEditingController(); + } + + + Future.microtask(() async { + managerId = ref.read(managerIdProvider); + roleId = ref.read(userRoleProvider); + userId = ref.read(userIdProvider); + + print('PD =>PayOutDetails => managerId: $managerId'); + + if (managerId != null) { + getAgentList(managerId); + } + + await getBroker(); + + // ✅ EDIT MODE (MUST be here) + if (widget.editItem != null) { + print('PD => editItemID ${widget.editItem}'); + + final invoiceID = widget.editItem!['id']; + + controllers['invoiceNo']?.text = + widget.editItem!['invoice_no']; + + controllers['invoiceDate']?.text = + widget.editItem!['invoice_date_ui_format']; + + controllers['invoiceStatus']?.text = + widget.editItem!['payout_status']; + + _loadEditData(invoiceID, managerId); // ✅ managerId ready + } + }); + } + + + void filterPolicyData(String query) { + final lowerQuery = query.toLowerCase(); + + print('PD => filterPolicyData - $lowerQuery'); + + print('PD => filteredPolicies1 - $filteredPolicies'); + setState(() { + if (query.trim().isEmpty) { + print('PD => filteredPolicies2'); + filteredPolicies = List.from(masterPolicies); + return; + } + print('PD => filteredPolicies3'); + filteredPolicies = masterPolicies.where((item) { + return (item['policy_no'] ?? '').toString().toLowerCase().contains( + lowerQuery, + ) || + (item['customer_name'] ?? '').toString().toLowerCase().contains( + lowerQuery, + ) || + (item['agent_name'] ?? '').toString().toLowerCase().contains( + lowerQuery, + ) || + (item['insurer_name'] ?? '').toString().toLowerCase().contains( + lowerQuery, + ) || + (item['premium_amount'] ?? '').toString().toLowerCase().contains( + lowerQuery, + ) || + (item['commission_amount'] ?? '').toString().toLowerCase().contains( + lowerQuery, + ) || + (_formatDate(item['issued_date']) ?? '').toLowerCase().contains( + lowerQuery, + ); + }).toList(); + }); + } + + void refresh() { + setState(() { + for (var controller in controllers.values) { + controller.clear(); + controller.dispose(); + } + + selectedBrokerID = ''; + selectedAgentId = []; + selectedPolicies = {}; + totalPolicies = ''; + totalCommission = ''; + filteredPolicies = []; + hasFetchedTableData = false; + // dropDownKeyPartner.currentState?.clear(); + }); + } + + void _loadEditData(String invoiceID, managerID) async { + setState(() => isLoadingEditData = true); + print("H010 => r : $roleId | mId: $managerID | ParmMID: $managerID | uId: $userId "); + print('PD => _loadEditData'); + + final jsondata = { + // "id": invoiceID, + "invoice_id": invoiceID, + "manager_id": managerId + + // "issued_date": formattedTillDate, + }; + print('PD => _loadEditData - $jsondata'); + final response = await apiService.getCommissionRateList(jsondata); + // final response = await apiService.getInvoiceDetails(invoiceID); + + if (response['status'] == 'success') { + print('PD => _loadEditData success'); + print('PD => _loadEditData success - ${response['data']}'); + final invoice = response['data']; + print("Invoice → $invoice"); + // final items = List>.from(response['data']['items']); + final items = response['data']; + final totPolicy = response['total_policies'].toString() ?? "0"; + final totalCommiss = response['total_commission']; + + setState(() { + // selectedPolicies = response['total_policies']; + isEdit = true; + totalPolicies = totPolicy; + totalCommission = totalCommiss; + + // filteredPolicies = items.map((p) { + // final policyId = int.parse(p['policy_id']); + // return {...p, 'is_selected': selectedPolicies.contains(policyId)}; + // }).toList(); + + final List> items = (response['data'] as List) + .map((e) => Map.from(e)) + .toList(); + + masterPolicies = List>.from(response['data']); + filteredPolicies = List>.from(response['data']); + + print("selectedPolicies → $filteredPolicies "); + }); + print( + "selectedPolicies → $selectedPolicies ->${response['total_policies']}", + ); + print("Items → $items"); + + // ----------------------------- + // 1. SET BASIC FIELDS + // ----------------------------- + // setState(() { + // // selectedAgentId = int.tryParse(invoice['agent_id'].toString()); + // + // // invoiceNoController.text = invoice['invoice_no']; + // // invoiceDate = DateTime.parse(invoice['invoice_date']); + // + // controllers['startDate']?.text = invoice['from_date']; + // controllers['endDate']?.text = invoice['to_date']; + // selectedBrokerID = int.tryParse(invoice['broker_id'].toString()); + // if (invoice['agent_id'] is List) { + // selectedAgentId = (invoice['agent_id'] as List) + // .map((e) => e.toString()) + // .toList(); + // } else if (invoice['agent_id'] is String) { + // final raw = invoice['agent_id'].toString(); + // + // if (raw.startsWith('[')) { + // // JSON string: "[1,2]" + // selectedAgentId = List.from( + // jsonDecode(raw).map((e) => e.toString()), + // ); + // } else if (raw.contains(',')) { + // // Comma separated: "1,2" + // selectedAgentId = raw.split(',').map((e) => e.trim()).toList(); + // } else if (raw.isNotEmpty) { + // // Single value: "1" + // selectedAgentId = [raw]; + // } else { + // selectedAgentId = []; + // } + // } else { + // selectedAgentId = []; + // } + // + // // if (invoice['till_date'] != "0000-00-00") { + // // policyTillDate = DateTime.parse(invoice['till_date']); + // // } else { + // // policyTillDate = DateTime.now(); + // // } + // }); + // print('PD => selectedBrokerID $selectedBrokerID'); + + // ----------------------------- + // 3. LOAD ALL POLICIES FOR THIS BROKER + AGENT + // ----------------------------- + // await loadPolicies(); + + // ----------------------------- + // 2. SET SELECTED POLICIES + // ----------------------------- + // selectedPolicies = items + // .map((p) => int.parse(p['policy_id'].toString())) + // .toSet(); + + // ----------------------------- + // 3. LOAD ALL POLICIES FOR THIS BROKER + AGENT + // ----------------------------- + // await loadPolicies(); + + // ----------------------------- + // 4. KEEP ONLY POLICIES THAT MATCH API DATA + // ----------------------------- + // setState(() { + // filteredPolicies = filteredPolicies.map((p) { + // final policyId = int.parse(p['policy_id']); + // return {...p, 'is_selected': selectedPolicies.contains(policyId)}; + // }).toList(); + // }); + } + + setState(() => isLoadingEditData = false); + } + + String formatDate(String date) { + final d = DateTime.parse(date); + return DateFormat('dd-MM-yyyy').format(d); + } + + // Get Broker List + Future getBroker() async { + print('PD =>getBroker called'); + setState(() { + isLoading = true; + }); + + try { + final response = await apiService.fetchMasterDropDown('Broker'); + + if (response['status'] == 200) { + print('PD =>getBroker - ${response['data']}'); + setState(() { + getBrokerData = List>.from(response['data']); + print('PD =>API Data - $getBrokerData'); + + filteredBrokerData = List.from(getBrokerData); + print('PD =>originalData - $filteredBrokerData'); + }); + } else { + getBrokerData = []; + filteredBrokerData = []; + } + } catch (e) { + print('PD =>Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + //Get Agent List + Future getAgentList(int id) async { + print('PD =>getClaimList called'); + setState(() { + isLoading = true; + }); + + try { + final response = await apiService.fetchAgentUserList(managerId); + + if (response['status'] == 'success') { + print('PD =>getAgentListData - ${response['data']}'); + setState(() { + getAgentData = List>.from(response['data']); + print('PD =>API Data - $getAgentData'); + + filteredAgentData = List.from(getAgentData); + print('PD =>originalData - $filteredAgentData'); + }); + } else { + getAgentData = []; + filteredAgentData = []; + } + } catch (e) { + print('PD =>Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + // Generate invoice number + String generateInvoiceNumber() { + final now = DateTime.now(); + final random = now.millisecondsSinceEpoch % 1000; + return "INV${now.year}${now.month.toString().padLeft(2, '0')}${random.toString().padLeft(3, '0')}"; + } + + // Pick date helper + Future pickDate(DateTime initial) async { + return await showDatePicker( + context: context, + initialDate: initial, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + } + + // if (dashboardKey == 'f' && + // isDashboardInitialLoad && + // SelectedStatus == "P") { + // + // + // fromDt = "";toDt = ""; + // isDashboardInitialLoad = false; + // + // } else if (dashboardKey == 'f' && isDashboardInitialLoad && SelectedStatus != '') { + // fromDt = "";toDt = ""; + // isDashboardInitialLoad = false; + // } else if (fromDateVal.isEmpty && toDateVal.isEmpty) { + // // Default fallback + // final today = DateTime.now(); + // fromDt = DateFormat('dd-MM-yyyy').format(today.subtract(const Duration(days: 5))); + // toDt = DateFormat('dd-MM-yyyy').format(today); + // } else { + // // ✅ USER SELECTED DATE — ALWAYS RESPECT THIS + // fromDt = fromDateVal; + // toDt = toDateVal; + // } + + // Load policies (simulate API) + Future loadPolicies() async { + print('PD =>loadPolicies'); + print("H13 => r : $roleId | mId: $managerId | uId: $userId "); + setState(() { + isLoading = true; + hasFetchedTableData = false; + }); + try { + final List agentIds = (selectedAgentId ?? []) + .map((e) => int.parse(e)) + .toList(); + + final jsondata = { + "from_date": controllers['startDate']?.text, + "to_date": controllers['endDate']?.text, + "manager_id": managerId, + if (agentIds != null && agentIds.isNotEmpty) "agent_id": agentIds, + // "agent_id": agentIds, + // "issued_date": formattedTillDate, + }; + final response = await apiService.getCommissionRateList(jsondata); + + if (response['status'] == 'success') { + print('PD =>API Data response - $response'); + setState(() { + filteredPolicies = List>.from(response['data']); + hasFetchedTableData = true; + print('PD =>API Data - $filteredPolicies'); + // filteredPolicies = allPolicies.where((p) { + // if (p["agentId"] != selectedAgentId) return false; + // if (policyTillDate != null && + // DateTime.parse(p["date"]).isAfter(policyTillDate!)) + // return false; + // return true; + // }).toList(); + }); + } else { + setState(() { + filteredPolicies = []; + hasFetchedTableData = true; + }); + } + } catch (e) { + print('PD =>Exception occurred: $e'); + setState(() { + filteredPolicies = []; + hasFetchedTableData = false; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + // Apply date filters + void filterPolicies() { + if (selectedAgentId == null) return; + + setState(() { + filteredPolicies = allPolicies.where((p) { + final date = DateTime.parse(p["date"]); + // if (p["agentId"] != selectedAgentId) return false; + // if (policyTillDate != null && date.isAfter(policyTillDate!)) + // return false; + if (fromDate != null && date.isBefore(fromDate!)) return false; + if (toDate != null && date.isAfter(toDate!)) return false; + return true; + }).toList(); + }); + } + + // Save invoice (simulation) + Future saveInvoice() async { + try { + if (selectedAgentId == null) { + ToastHelper.showWarningToast(context, "Please select an agent"); + return; + } + + if (selectedPolicies.isEmpty) { + ToastHelper.showWarningToast( + context, + "Please select at least 1 policy", + ); + return; + } + + // Format dates for API + final String formattedInvoiceDate = DateFormat( + 'yyyy-MM-dd', + ).format(invoiceDate); + final String formattedTillDate = DateFormat( + 'yyyy-MM-dd', + ).format(policyTillDate!); + + // Total Commission + final double totalCommission = filteredPolicies + .where((p) => selectedPolicies.contains(int.parse(p["policy_id"]))) + .fold(0.0, (sum, p) => sum + _parseCommissionValue(p["commission_amount"])); + + // Build items array + final List> items = filteredPolicies + .where((p) => selectedPolicies.contains(int.parse(p["policy_id"]))) + .map( + (p) => { + "policy_id": int.parse(p["policy_id"]), + "policy_no": p["policy_no"], + "commission_amount": _parseCommissionValue(p["commission_amount"]), + }, + ) + .toList(); + final List> utrs = _buildUtrPayloadForSelectedPolicies( + defaultUtrDate: formattedInvoiceDate, + ); + + final jsondata = { + "invoice_no": controllers['invoiceNo']?.text, + "invoice_amount": totalCommission, + "agent_id": selectedAgentId, + "broker_id": selectedBrokerID, + "invoice_date": formattedInvoiceDate, + "till_date": formattedTillDate, + "payout_status": 0, + "pos_id": null, + "created_by": userId, + "updated_by": userId, + "items": items, + "utrs": utrs, + }; + print('PD =>Commissions $jsondata'); + final bool confirm = await showSaveConfirmation(context); + if (!confirm) return; + + final response = await apiService.getCreateOrUpdate(jsondata); + + if (response['status'] == 'success') { + setState(() { + print('PD =>API Data response - $response'); + selectedPolicies.clear(); + controllers['invoiceNo']?.clear(); + invoiceDate = DateTime.now(); + policyTillDate = DateTime.now(); + filteredPolicies = []; + _uploadedUtrByPolicyKey.clear(); + }); + context.go(AppRoutes.invoiceList); + } else { + filteredPolicies = []; + } + } catch (e) { + print('PD =>Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + String _normalizeHeader(dynamic header) { + return header + .toString() + .trim() + .toLowerCase() + .replaceAll(RegExp(r'[^a-z0-9]'), ''); + } + + String _excelCellValue(Data? cell) { + final value = cell?.value; + if (value == null) return ''; + return value.toString().trim(); + } + + String _normalizePolicyNo(dynamic value) { + return (value ?? '') + .toString() + .trim() + .toLowerCase() + .replaceAll(RegExp(r'\s+'), ''); + } + + String _displayText(dynamic value) { + if (value == null) return '-'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '-'; + return text; + } + + double? _parseExcelCommission(String raw) { + final cleaned = raw + .replaceAll('₹', '') + .replaceAll(',', '') + .replaceAll(RegExp(r'\s+'), '') + .trim(); + if (cleaned.isEmpty) return null; + return double.tryParse(cleaned); + } + + List> _buildUtrPayloadForSelectedPolicies({ + required String defaultUtrDate, + }) { + /* + * Build UTR payload from selected policies. + * 1) UTR source is Excel "UTR Number" column captured per policy. + * 2) If same UTR appears in multiple selected rows, amount is aggregated. + * 3) Amount comes from selected policy commission amount. + */ + final Map utrAmountMap = {}; + + for (final policy in filteredPolicies) { + final int policyId = int.tryParse(policy["policy_id"]?.toString() ?? "0") ?? 0; + if (!selectedPolicies.contains(policyId)) continue; + + final String policyKey = _normalizePolicyNo(policy['policy_no']); + final String utrNo = ((_uploadedUtrByPolicyKey[policyKey] ?? + policy['utr_no'] ?? + '') + .toString()) + .trim(); + if (utrNo.isEmpty) continue; + + final double amount = _parseCommissionValue(policy["commission_amount"]); + utrAmountMap[utrNo] = (utrAmountMap[utrNo] ?? 0) + amount; + } + + return utrAmountMap.entries + .map( + (e) => { + "utr_no": e.key, + "amount": e.value, + "utr_date": defaultUtrDate, + }, + ) + .toList(); + } + + Widget _excelMatchedPolicyTile(Map row) { + return Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE5E7EB)), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${row['policy_no']} | ${row['current_commission']} -> ${row['excel_commission']}', + style: GoogleFonts.inter(fontSize: 12), + ), + ], + ), + ); + } + + /// No Excel row matched the table — list all Excel policy numbers; OK only. + Future _showAllPoliciesUnmatchedDialog({ + required List excelPolicyNumbersNotMatched, + required int invalidExcelRows, + }) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: Text( + 'No policies matched', + style: GoogleFonts.inter(fontWeight: FontWeight.w600), + ), + content: SizedBox( + width: 520, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'None of the policy numbers in the Excel file match the policies in the current table.', + style: GoogleFonts.inter(fontSize: 13, height: 1.35), + ), + if (excelPolicyNumbersNotMatched.isNotEmpty) ...[ + const SizedBox(height: 12), + Text( + 'Policy numbers from Excel (not matched):', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + ...excelPolicyNumbersNotMatched.map( + (p) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text('• $p', style: GoogleFonts.inter(fontSize: 13)), + ), + ), + ], + if (invalidExcelRows > 0) ...[ + const SizedBox(height: 12), + Text( + '$invalidExcelRows row(s) in Excel were skipped (invalid policy number or commission).', + style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]), + ), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('OK', style: GoogleFonts.inter(fontWeight: FontWeight.w600)), + ), + ], + ), + ); + } + + /// At least one match — show matched one-by-one; if Excel has extra policies, list them; Proceed / Cancel. + Future _showUploadReviewProceedDialog({ + required List> matchedRows, + required List excelPolicyNumbersNotOnTable, + required int invalidExcelRows, + }) async { + final hasExcelOnly = excelPolicyNumbersNotOnTable.isNotEmpty; + return await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: Text( + hasExcelOnly ? 'Review Excel vs table' : 'Confirm commission update', + style: GoogleFonts.inter(fontWeight: FontWeight.w600), + ), + content: SizedBox( + width: 560, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Matched policies (${matchedRows.length})', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + ...matchedRows.map(_excelMatchedPolicyTile), + if (hasExcelOnly) ...[ + const SizedBox(height: 16), + Text( + 'Not found on current table (${excelPolicyNumbersNotOnTable.length})', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.orange.shade800, + ), + ), + const SizedBox(height: 6), + Text( + 'These policy numbers are in Excel but not in the loaded table:', + style: GoogleFonts.inter(fontSize: 12, height: 1.35), + ), + const SizedBox(height: 8), + ...excelPolicyNumbersNotOnTable.map( + (p) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + '• $p', + style: GoogleFonts.inter(fontSize: 13), + ), + ), + ), + ], + if (invalidExcelRows > 0) ...[ + const SizedBox(height: 12), + Text( + 'Note: $invalidExcelRows Excel row(s) skipped (invalid data).', + style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]), + ), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text('Cancel', style: GoogleFonts.inter()), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text('Proceed', style: GoogleFonts.inter()), + ), + ], + ), + ) ?? + false; + } + + Future _showMismatchProceedDialog(int mismatchCount) async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('Partner mismatch found'), + content: Text( + '$mismatchCount row(s) are linked to a different partner.\n' + 'Proceeding will retroactively update partner mapping in payout records.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Proceed'), + ), + ], + ), + ) ?? + false; + } + + Future _triggerBulkUpload() async { + if (selectedAgentId == null || selectedAgentId!.isEmpty) { + ToastHelper.showWarningToast(context, 'Please select Partner before uploading.'); + return; + } + if (selectedAgentId!.length != 1) { + ToastHelper.showWarningToast(context, 'Please select only 1 Partner before uploading.'); + return; + } + if (!hasFetchedTableData || filteredPolicies.isEmpty) { + ToastHelper.showWarningToast( + context, + 'No policy data loaded. Set dates, Partner, tap Filter, then upload.', + ); + return; + } + + try { + final picked = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xlsx', 'xls'], + withData: true, + ); + if (picked == null || picked.files.isEmpty) { + ToastHelper.showWarningToast(context, 'No Excel file selected. Please choose a file to upload.'); + return; + } + final selectedFile = picked.files.single; + if (selectedFile.bytes == null || selectedFile.bytes!.isEmpty) { + ToastHelper.showWarningToast(context, 'Unable to read the selected Excel file. Try another file.'); + return; + } + + setState(() => isLoading = true); + final excel = Excel.decodeBytes(Uint8List.fromList(selectedFile.bytes!)); + if (excel.tables.isEmpty) { + ToastHelper.showWarningToast(context, 'Invalid Excel: no sheet found in the file.'); + return; + } + + final rows = excel.tables.values.first.rows; + if (rows.isEmpty) { + ToastHelper.showWarningToast(context, 'Uploaded Excel file is empty.'); + return; + } + + int policyNoCol = -1; + int commissionCol = -1; + int agentCodeCol = -1; + for (int i = 0; i < rows.first.length; i++) { + final normalized = _normalizeHeader(_excelCellValue(rows.first[i])); + if (normalized == 'policynumber') policyNoCol = i; + if (normalized == 'payoutamount' || normalized == 'commissionamount') commissionCol = i; + if (normalized == 'agentcode') agentCodeCol = i; + } + if (policyNoCol == -1 || commissionCol == -1) { + ToastHelper.showWarningToast( + context, + 'Excel is missing required columns: Policy Number and Payout Amount.', + ); + return; + } + + final Map uploadMap = {}; + final Map uploadPolicyDisplay = {}; + final Map uploadAgentCodeMap = {}; + int invalidRows = 0; + for (int rowIndex = 1; rowIndex < rows.length; rowIndex++) { + final row = rows[rowIndex]; + final policyRaw = row.length > policyNoCol ? _excelCellValue(row[policyNoCol]) : ''; + final commissionRaw = row.length > commissionCol ? _excelCellValue(row[commissionCol]) : ''; + final agentCodeRaw = agentCodeCol >= 0 && row.length > agentCodeCol + ? _excelCellValue(row[agentCodeCol]) + : ''; + if (policyRaw.trim().isEmpty && commissionRaw.trim().isEmpty && agentCodeRaw.trim().isEmpty) { + continue; + } + + final normalizedPolicy = _normalizePolicyNo(policyRaw); + final parsedCommission = _parseExcelCommission(commissionRaw); + if (normalizedPolicy.isEmpty || parsedCommission == null) { + invalidRows++; + continue; + } + uploadMap[normalizedPolicy] = parsedCommission; + uploadPolicyDisplay[normalizedPolicy] = + policyRaw.trim().isEmpty ? normalizedPolicy : policyRaw.trim(); + if (agentCodeRaw.trim().isNotEmpty) { + uploadAgentCodeMap[normalizedPolicy] = agentCodeRaw.trim().toUpperCase(); + } + } + + if (uploadMap.isEmpty) { + ToastHelper.showWarningToast( + context, + 'No valid data rows in Excel (check policy numbers and payout amounts).', + ); + return; + } + + final Set screenPolicyKeys = { + for (final p in filteredPolicies) _normalizePolicyNo(p['policy_no']), + }; + final List> matchedPolicyRefs = []; + final List> matchedRowsForDialog = []; + final List excelPoliciesNotOnTable = []; + int partnerMismatchCount = 0; + + for (final e in uploadMap.entries) { + final key = e.key; + if (screenPolicyKeys.contains(key)) { + final policy = filteredPolicies.firstWhere( + (p) => _normalizePolicyNo(p['policy_no']) == key, + ); + final excelAgentCode = (uploadAgentCodeMap[key] ?? '').trim(); + final tableAgentCode = + (policy['agent_code'] ?? '').toString().trim().toUpperCase(); + if (excelAgentCode.isNotEmpty && + tableAgentCode.isNotEmpty && + excelAgentCode != tableAgentCode) { + partnerMismatchCount++; + } + matchedPolicyRefs.add(policy); + matchedRowsForDialog.add({ + 'policy_no': (policy['policy_no'] ?? '').toString(), + 'current_commission': (policy['commission_amount'] ?? '').toString(), + 'excel_commission': e.value.toStringAsFixed(2), + }); + } else { + excelPoliciesNotOnTable.add(uploadPolicyDisplay[key] ?? key); + } + } + + if (matchedRowsForDialog.isEmpty) { + await _showAllPoliciesUnmatchedDialog( + excelPolicyNumbersNotMatched: excelPoliciesNotOnTable.isNotEmpty + ? excelPoliciesNotOnTable + : uploadMap.keys.map((k) => uploadPolicyDisplay[k] ?? k).toList(), + invalidExcelRows: invalidRows, + ); + return; + } + + final bool proceed = await _showUploadReviewProceedDialog( + matchedRows: matchedRowsForDialog, + excelPolicyNumbersNotOnTable: excelPoliciesNotOnTable, + invalidExcelRows: invalidRows, + ); + if (!proceed) { + ToastHelper.showWarningToast(context, 'Commission update cancelled'); + return; + } + + if (partnerMismatchCount > 0) { + final bool proceedMismatch = await _showMismatchProceedDialog( + partnerMismatchCount, + ); + if (!proceedMismatch) { + ToastHelper.showWarningToast(context, 'Cancelled due to partner mismatch.'); + return; + } + } + + final response = await apiService.uploadPolicyCommissionExcel( + id: selectedAgentId!.first, + file: selectedFile, + proceedPartnerMismatch: partnerMismatchCount > 0, + ); + if (response['status'] != 'success' && response['status'] != 200) { + ToastHelper.showErrorToast( + context, + (response['message'] ?? 'Upload failed').toString(), + ); + return; + } + + setState(() { + for (final policy in matchedPolicyRefs) { + final key = _normalizePolicyNo(policy['policy_no']); + if (uploadMap.containsKey(key)) { + policy['commission_amount'] = uploadMap[key]!.toStringAsFixed(2); + } + } + }); + _calculateTotalCommission(); + + final int tableRowsNotInExcel = filteredPolicies + .where((p) => !uploadMap.containsKey(_normalizePolicyNo(p['policy_no']))) + .length; + final message = + 'Updated ${matchedRowsForDialog.length} policy commission(s) from Excel' + '${partnerMismatchCount > 0 ? ' | partner mismatch updated: $partnerMismatchCount' : ''}' + '${excelPoliciesNotOnTable.isNotEmpty ? ' | Excel-only (ignored): ${excelPoliciesNotOnTable.length}' : ''}' + '${tableRowsNotInExcel > 0 ? ' | table rows not in Excel: $tableRowsNotInExcel' : ''}' + '${invalidRows > 0 ? ' | invalid Excel rows: $invalidRows' : ''}'; + ToastHelper.showSuccessToast(context, message); + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) setState(() => isLoading = false); + } + } + + void showMessage(String msg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + } + + Future showSaveConfirmation(BuildContext context) async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + title: Text( + "Confirm Save", + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + content: Text( + "Once saved, changes cannot be edited.\nAre you sure you want to continue?", + style: GoogleFonts.inter(fontSize: 13, color: Colors.grey[700]), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text( + "No", + style: GoogleFonts.inter( + fontWeight: FontWeight.w500, + color: Colors.grey[700], + ), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF059669), // emerald + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () => Navigator.pop(context, true), + child: Text( + "Yes, Save", + style: GoogleFonts.inter( + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ], + ); + }, + ) ?? + false; + } + + @override + Widget build(BuildContext context) { + final df = DateFormat("dd-MM-yyyy"); + final int status = + int.tryParse(controllers['invoiceStatus']?.text ?? '0') ?? 0; + + final bool isPending = status == 1; + + return MainLayout( + title: "Pay Out", + body: !isLoadingEditData + ? Container( + color: Colors.white, + padding: const EdgeInsets.all(8), + margin: isEdit + ? EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.1, + ) + : null, + child: Column( + children: [ + // const SizedBox(height: 20), + Container( + height: 40, + + width: MediaQuery.of(context).size.width, + child: GestureDetector( + onTap: null, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Tooltip( + message: 'Back', + child: IconButton( + icon: const Icon( + Icons.arrow_left_sharp, + size: 25, + color: Color(0xFF425B5B), + ), + onPressed: () { + context.go(AppRoutes.payoutList); + }, + splashRadius: 18, + hoverColor: Colors.black12, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), + ), + const SizedBox( + width: 5, + ), // spacing between icon and text + Text( + "Payout Details", + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + + // 🟢 Status Chip + if (isEdit) ...[ + Spacer(), + Text( + controllers['invoiceNo']?.text ?? '', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: const Color(0xFF374151), + ), + ), + SizedBox(width: 10), + Text( + // controllers['invoiceDate']?.text ?? '', + '(${controllers['invoiceDate']?.text ?? ''})', + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w300, + color: const Color(0xFF6B7280), + ), + ), + // SizedBox(width: 10), + // Spacer(), + SizedBox(width: 10), + Text( + 'Status: ${isPending ? 'Pending' : 'Completed'}', + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w300, + color: const Color(0xFF6B7280), + // color: isPending + // ? Colors.blueAccent + // : const Color(0xFF047857), + ), + ), + + Spacer(), + + ThemedSearchField( + hintText: 'Search', + + // backgroundColor: Color(0xFFF6F8F8), + onChanged: filterPolicyData, + + controller: _searchStaffController, + backgroundColor: Color(0xFFFFFFFF), + txtHeight: 30, + txtwidth: ResponsiveLayout.isMobile(context) + ? MediaQuery.of(context).size.width * 0.7 + : MediaQuery.of(context).size.width * 0.15, + ), + ], + + if (!isEdit) ...[ + const Spacer(), + OutlinedButton.icon( + onPressed: isLoading ? null : _triggerBulkUpload, + icon: const Icon(Icons.upload_file, size: 16), + label: Text( + "Upload", + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Color(0xFF2E7D6E)), + foregroundColor: const Color(0xFF2E7D6E), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + ), + ), + if (hasFetchedTableData) ...[ + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: const Color(0xFFECFDF5), // emerald bg + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Text( + "SELECTED POLICIES : ", + style: GoogleFonts.poppins( + fontSize: 10, + fontWeight: FontWeight.w500, + color: const Color(0xFF065F46), + letterSpacing: 0.4, + ), + ), + const SizedBox(width: 6), + Text( + totalPolicies, + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w700, + color: const Color(0xFF047857), + ), + ), + ], + ), + ), + + const SizedBox(width: 10), + + // 💰 TOTAL COMMISSION + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: const Color(0xFF10B981), // emerald green + borderRadius: BorderRadius.circular(22), + boxShadow: [ + BoxShadow( + color: const Color( + 0xFF10B981, + ).withOpacity(0.25), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + const Icon( + Icons.account_balance_wallet_outlined, + size: 14, + color: Colors.white, + ), + const SizedBox(width: 6), + Text( + "TOTAL COMMISSION (PAYOUT) :", + style: GoogleFonts.poppins( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Colors.white.withOpacity(0.9), + letterSpacing: 0.4, + ), + ), + const SizedBox(width: 6), + Text( + "₹$totalCommission", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + ], + ], + ], + ), + ), + ), + + // SizedBox(height: 5), + if (!isEdit) ...[ + Container( + padding: EdgeInsets.all(8.0), + child: DateFilterRowPayout( + formKey: _formKey, + // key: ValueKey(SelectedStatus ?? ''), + role: roleId, + id: userId, + showBrokerFilter: false, + selectedParnter: selectedAgentId, + selectedBroker: selectedBrokerID, + + onPartnerChanges: (val) { + setState(() { + selectedAgentId = val; // ✅ parent state updated + }); + }, + selectedStaffId: '', + startController: controllers['startDate']!, + endController: controllers['endDate']!, + + isMobile: ResponsiveLayout.isMobile(context), + onFilter: () { + print( + 'DATEROw- ${controllers['startDate']!} -${controllers['endDate']!}' + ' - $selectedAgentId', + ); + // call your filter logic + // filterDateRange(); + + loadPolicies(); + }, + onRefresh: () { + // call your refresh logic + refresh(); + }, + ), + ), + + SizedBox(height: 10), + ], + // Policy Selection + Card( + // color: Colors.amber, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + child: Column( + children: [ + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: policyCollapsed + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: const SizedBox(), + secondChild: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: _tableMinWidth, + child: Column( + children: [ + _policyTableHeader(), // static header + _policyTableBody(), // vertical scroll rows + ], + ), + ), + ), + ), + ], + ), + ), + + if (selectedPolicies.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.all(8), + // color: Colors.amber, + color: Color(0xFFf9fafb), + // color: Colors.white, + child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // ---------------- SELECTED COUNT ---------------- + Row( + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + // selectedPolicies.length.toString(), + totalPolicies, + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + Text( + "policies selected", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + Text( + "|", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + Text( + "Total Commission (Payout)", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + + /// NULL-SAFE TOTAL COMMISSION CALCULATION + Text( + // "₹ ${_calculateTotalCommission()}", + "₹ $totalCommission", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.teal, + ), + ), + ], + ), + + Spacer(), + SizedBox(width: 20), + // ---------------- BUTTONS ---------------- + Row( + children: [ + ElevatedButton( + onPressed: saveInvoice, + style: ElevatedButton.styleFrom( + // backgroundColor: const Color(0xff45a049), + backgroundColor: Colors.teal, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 4, + ), // 🔽 reduced radius + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + ), + child: Text( + "Raise Invoice", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(width: 10), + TextButton( + onPressed: () => + setState(() => selectedPolicies.clear()), + child: const Text("Clear"), + ), + ], + ), + ], + ), + ), + ], + + if (isEdit) ...[ + Container( + padding: const EdgeInsets.all(8), + // color: Colors.amber, + color: Color(0xFFf9fafb), + // color: Colors.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + // selectedPolicies.length.toString(), + totalPolicies, + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + Text( + "policies selected", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + Text( + "|", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + Text( + "Total Commission (Payout)", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + ), + ), + const SizedBox(width: 4), + + /// NULL-SAFE TOTAL COMMISSION CALCULATION + Text( + // "₹ ${_calculateTotalCommission()}", + "₹ $totalCommission", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.teal, + ), + ), + + const SizedBox(width: 10), + ], + ), + ), + ], + // ---------------------- + // const SizedBox(height: 100), + ], + ), + ) + : Container( + color: Colors.white, + height: MediaQuery.of(context).size.height * 0.7, + child: Center(child: CircularProgressIndicator()), + ), + ); + } + + Widget _policyTableHeader() { + final allSelected = + filteredPolicies.isNotEmpty && + filteredPolicies.every( + (p) => selectedPolicies.contains( + int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0, + ), + ); + // filteredPolicies.every( + // (p) => selectedPolicies.contains( + // int.tryParse(p["policy_id"].toString()) ?? 0, + // ), + // ); + + final partiallySelected = selectedPolicies.isNotEmpty && !allSelected; + + return Container( + height: 30, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: const BoxDecoration( + // color: Color(0xFFf9fafb), + color: Color(0xFFf9fafb), + // color: Color(0xFFf9fafb), + // color: Color(0xFFF6F8F7), + // border: Border(bottom: BorderSide(color: Color(0xFFE0E0E0))), + // border: Border(bottom: BorderSide(color: Color(0xFFECEFF1))), + border: Border(bottom: BorderSide(color: Color(0xFFf9fafb))), + ), + child: Row( + children: [ + if (!isEdit) ...[ + SizedBox( + width: colCheck, + child: Checkbox( + value: allSelected ? true : (partiallySelected ? null : false), + tristate: true, + onChanged: (v) { + setState(() { + if (v == true) { + selectedPolicies = filteredPolicies + .map( + (p) => + int.tryParse( + p["policy_id"]?.toString() ?? "0", + ) ?? + 0, + ) + .where((id) => id != 0) // Filter out invalid IDs + .toSet(); + // selectedPolicies = filteredPolicies + // .map( + // (p) => int.tryParse(p["policy_id"].toString()) ?? 0, + // ) + // .toSet(); + } else { + selectedPolicies.clear(); + } + totalPolicies = selectedPolicies.length.toString(); + }); + _calculateTotalCommission(); + }, + ), + ), + ], + _headerText("POLICY NO", colPolicy), + _headerText("POLICY DATE", colDate), + _headerText("PARTNER", colAgent), + _headerText("CUSTOMER", colCustomer), + _headerText("PREMIUM", colPremium), + _headerText("COMMISSION (PAYOUT)", colCommission), + ], + ), + ); + } + + Widget _headerText(String text, double width) { + return SizedBox( + width: width, + child: Text( + text, + style: GoogleFonts.poppins( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Colors.blueGrey, + letterSpacing: 0.5, + ), + ), + ); + } + + Widget _policyTableBody() { + return SizedBox( + // height: isEdit ? 400 : 340, // avoids overflow + height: isEdit + ? MediaQuery.of(context).size.height * 0.65 + : MediaQuery.of(context).size.height * 0.48, // avoids overflow + child: ListView.builder( + itemCount: filteredPolicies.length, + itemBuilder: (context, index) { + final p = filteredPolicies[index]; + + final int id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0; + final bool isSelected = selectedPolicies.contains(id); + + return InkWell( + onTap: () { + setState(() { + isSelected + ? selectedPolicies.remove(id) + : selectedPolicies.add(id); + totalPolicies = selectedPolicies.length.toString(); + }); + _calculateTotalCommission(); + }, + child: Container( + height: 38, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + // const Color(0xFFE6F4EF) + color: isSelected ? const Color(0xFFF0FAF6) : Colors.white, + border: const Border( + bottom: BorderSide(color: Color(0xFFf9fafb)), + // bottom: BorderSide(color: Color(0xFFEDEDED)), + ), + ), + child: Row( + children: [ + if (!isEdit) ...[ + SizedBox( + width: colCheck, + child: Checkbox( + value: isSelected, + onChanged: (v) { + setState(() { + v == true + ? selectedPolicies.add(id) + : selectedPolicies.remove(id); + totalPolicies = selectedPolicies.length.toString(); + }); + _calculateTotalCommission(); + }, + ), + ), + ], + _cell(p["policy_no"], colPolicy), + // _cell(p["issued_date"] ?? "-", colDate), + _cell(_formatDate(p['issued_date']), colDate), + // _cell(p["agent_name"] ?? "-", colAgent), + _cell( + (p["agent_code"] != null && p["agent_name"] != null) + ? '${p["agent_code"]} - ${p["agent_name"]}' + : (p["agent_name"] ?? '-'), + colAgent, + ), + _cell(p["customer_name"], colCustomer), + _cell("₹${p["premium_amount"]}", colPremium), + SizedBox( + width: colCommission, + child: Row( + children: [ + const Text( + "₹", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF009B77), + ), + ), + const SizedBox(width: 2), + Expanded( + child: TextFormField( + key: ValueKey( + 'commission_${p["policy_id"]}_${p["commission_amount"]}', + ), + initialValue: (p["commission_amount"] ?? '') + .toString(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF009B77), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + inputFormatters: _commissionInputFormatter, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 6, + vertical: 6, + ), + border: OutlineInputBorder(), + ), + onChanged: (value) { + p["commission_amount"] = value.trim(); + _calculateTotalCommission(); + }, + onFieldSubmitted: (value) => + _updateInlineCommission(p, value), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + double _parseCommissionValue(dynamic value) { + if (value == null) return 0; + return double.tryParse(value.toString().trim()) ?? 0; + } + + Future _updateInlineCommission(Map row, String value) async { + final trimmed = value.trim(); + final amount = double.tryParse(trimmed); + if (amount == null) { + ToastHelper.showWarningToast(context, 'Invalid payout amount'); + return; + } + + final int id = + int.tryParse((row['id'] ?? row['policy_id'] ?? '').toString()) ?? 0; + if (id == 0 || _inlineSavingIds.contains(id)) return; + + setState(() { + _inlineSavingIds.add(id); + }); + + try { + final response = await apiService.createUserData( + { + 'id': id, + 'commission_amount': amount, + }, + 'policy/payoutInilneEditUpdate', + ); + if (response['status'] == 'success' || response['status'] == 200) { + row['commission_amount'] = trimmed; + _calculateTotalCommission(); + } else { + ToastHelper.showErrorToast( + context, + (response['message'] ?? 'Inline update failed').toString(), + ); + } + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) { + setState(() { + _inlineSavingIds.remove(id); + }); + } + } + } + + Widget _cell(String? value, double width) { + return SizedBox( + width: width, + child: Text( + value ?? "-", + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 11), + ), + ); + } + + double _calculateTotalCommission() { + print("ASD"); + print(filteredPolicies); + double total = 0.0; + + for (var p in filteredPolicies) { + // Safe policy_id parsing + final String idStr = p["policy_id"]?.toString() ?? "0"; + final int id = int.tryParse(idStr) ?? 0; + + if (!selectedPolicies.contains(id)) continue; + + total += _parseCommissionValue(p["commission_amount"]); + } + setState(() { + totalCommission = total.roundToDouble(); + }); + // Remove decimal points (.00) + return total.roundToDouble(); + } + + InputDecoration commonInputDecoration({required String hint}) { + return InputDecoration( + hintText: hint, + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide( + color: Color(0xFFDDDDDD), // light gray + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide( + color: Color(0xFFAAAAAA), // darker gray on focus + width: 1.2, + ), + ), + ); + } + + static final _textStyle = GoogleFonts.poppins( + fontSize: 11, + fontWeight: FontWeight.w500, + ); + + String _formatDate(String rawDate) { + try { + final dateTime = DateTime.parse(rawDate); + return DateFormat('dd-MM-yyyy').format(dateTime); // 24-hour format + } catch (e) { + return rawDate; // fallback if parsing fails + } + } + + static final double colCheck = 44; + double get colPolicy => isEdit ? 150 : 200; + double get colDate => isEdit ? 120 : 200; + static final double colAgent = 200; + static final double colCustomer = 200; + static final double colPremium = 200; + // static final double colCommission = 200; + + double get colCommission => isEdit ? 120 : 200; + double get _tableMinWidth => + (isEdit ? 0 : colCheck) + + colPolicy + + colDate + + colAgent + + colCustomer + + colPremium + + colCommission + + 32; +} \ No newline at end of file diff --git a/lib/presentation/screens/payout/payout_list.dart b/lib/presentation/screens/payout/payout_list.dart new file mode 100644 index 0000000..e033e29 --- /dev/null +++ b/lib/presentation/screens/payout/payout_list.dart @@ -0,0 +1,1852 @@ +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:intl/intl.dart'; +import 'package:nhance_partner/data/utils/toastNotification.dart'; + +import '../../../core/routing/routes.dart'; +import '../../../core/services/api_service.dart'; +import '../../../data/utils/Pagination.dart'; +import '../../layouts/main_layout.dart'; +import '../../layouts/responsive_layout.dart'; +import '../../providers/manager_provider.dart'; +import '../../providers/userRoleProvider.dart'; +import '../../themes/indicators/export_btn.dart'; +import '../../themes/indicators/search_field_theme.dart'; +import '../../widgets/custom_Stdate_EnDate_Filter.dart'; + +class PayoutList extends ConsumerStatefulWidget { + const PayoutList({super.key}); + + @override + ConsumerState createState() => _PayoutListState(); +} + +class _PayoutListState extends ConsumerState { + int currentPage = 1; + int itemsPerPage = 10; + bool isLoading = false; + List> getPayoutData = []; + List> originalData = []; + List> filteredData = []; + dynamic roleId; + dynamic userId; + late ApiService apiService; + final _formKey = GlobalKey(); + Map controllers = {}; + List tabHeader = ['startDate', 'endDate']; + dynamic SelectedStatus; + dynamic SelectedStaffId; + DateTime _paymentDate = DateTime.now(); + dynamic _hoveredHistoryInvoiceId; + static const double _actionIconSize = 12; + static const double _actionButtonSize = 28; + static const double _actionIconGap = 2; + + @override + void initState() { + super.initState(); + apiService = ApiService(); + for (String field in tabHeader) { + controllers[field] = TextEditingController(); + } + + Future.microtask(() { + final id = ref.read(managerIdProvider); + roleId = ref.read(userRoleProvider); + userId = ref.read(userIdProvider); + + print("PL 1 => r : $roleId | mId: $id | uId: $userId "); + getPayoutList(); + }); + } + + // Get Payout List + Future getPayoutList() async { + print('PL 2 => get Payout Listing Called'); + setState(() { + isLoading = true; + }); + + try { + final response = await apiService.getPayoutList(); + + if (response['status'] == 'success') { + print('PL 3 => getPayoutList - ${response['data']}'); + final data = response['data']; + print(' PL 4 => getPayoutList => ${response['data']}'); + + setState(() { + + if (data is List) { + // Already a list of maps + getPayoutData = List>.from(data); + } else if (data is Map) { + // Single object, wrap in a list + getPayoutData = [Map.from(data)]; + } else { + getPayoutData = []; + } + originalData = getPayoutData; + filteredData = List.from(originalData); + }); + } else { + originalData = []; + filteredData = []; + } + } catch (e) { + print('PL 5 => Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + // Delete Invoice List + Future deleteInvoice(ID) async { + setState(() { + isLoading = true; + }); + + try { + final response = await apiService.deleteInvoice(ID); + + if (response['status'] == 'success') { + ToastHelper.showSuccessToast(context, response['data']); + getPayoutList(); + } else { + ToastHelper.showSuccessToast(context, response['data']); + } + } catch (e) { + print('PL 6 => Exception occurred: $e'); + } finally { + setState(() { + isLoading = false; + }); + } + } + + List get _paginatedData { + // Sort descending by id first + final sortedData = [...filteredData] + ..sort((a, b) => int.parse(b['id']) - int.parse(a['id'])); + + if (sortedData.isEmpty) return []; + + // Ensure currentPage is valid + final maxPage = (sortedData.length / itemsPerPage).ceil(); + final safePage = currentPage.clamp(1, maxPage); + + final startIndex = (safePage - 1) * itemsPerPage; + final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length); + + return sortedData.sublist(startIndex, endIndex); + } + + void filterDateRange() {} + + void refrshfilterDateRange() { + setState(() { + // SelectedStaffId = null; + controllers['startDate']!.clear(); + controllers['endDate']!.clear(); + + controllers['startDate']?.text = ''; + controllers['endDate']?.text = ''; + // Reset the FormField validation + _formKey.currentState?.reset(); + }); + getPayoutList(); + } + + void filterData(String query) { + print("PL 7 =>FilterDAta - $query"); + final q = query.toLowerCase(); + setState(() { + filteredData = getPayoutData.where((item) { + // Search against the same display strings as the table (not raw API values). + final updatedAtSearch = _exportUpdatedAtForExcel(item['updated_at']) + .toLowerCase(); + final invoiceDateSearch = _formatDateSafe(item['invoice_date']) + .toLowerCase(); + final statusSearch = + _exportPayoutStatusLabel(item['payout_status']).toLowerCase(); + + return updatedAtSearch.contains(q) || + invoiceDateSearch.contains(q) || + (item['invoice_no'] ?? '-').toLowerCase().contains(q) || + (item['invoice_amount_indian_format'] ?? '-') + .toLowerCase() + .contains(q) || + (item['broker_name'] ?? '-').toLowerCase().contains(q) || + (item['partner_names'] ?? '-').toLowerCase().contains(q) || + (item['utr_numbers'] ?? '-').toLowerCase().contains(q) || + (item['invoiced_amount'] ?? '0').toLowerCase().contains(q) || + (item['payout_amount'] ?? '0').toLowerCase().contains(q) || + (item['balance_amount'] ?? '0').toLowerCase().contains(q) || + statusSearch.contains(q) || + // Also match raw codes if user types "1" or "2" + (item['payout_status'] ?? '-').toString().toLowerCase().contains(q); + }).toList(); + }); + } + + String _formatDateSafe(dynamic raw) { + if (raw == null) return '-'; + try { + final dateTime = DateTime.parse(raw.toString()); + return DateFormat('dd-MM-yyyy').format(dateTime); + } catch (_) { + return raw.toString(); + } + } + + final TextEditingController _searchStaffController = TextEditingController(); + + String _formatDate(String rawDate) { + try { + final dateTime = DateTime.parse(rawDate); + return DateFormat('dd-MM-yyyy').format(dateTime); // 24-hour format + } catch (e) { + return rawDate; // fallback if parsing fails + } + } + + String _formatTime(String rawDate) { + try { + final dateTime = DateTime.parse(rawDate); + return DateFormat('HH:mm').format(dateTime); + } catch (e) { + return '-'; + } + } + + /// Excel export: match table (1 = Pending, 2 = Completed). + String _exportPayoutStatusLabel(dynamic v) { + if (v == null) return '-'; + final s = v.toString().trim(); + if (s.isEmpty || s.toLowerCase() == 'null') return '-'; + if (s == '1') return 'Pending'; + if (s == '2') return 'Completed'; + return 'Completed'; + } + + /// Excel export: same as table — dd-MM-yyyy + 24h time in one cell. + String _exportUpdatedAtForExcel(dynamic v) { + if (v == null) return '-'; + final raw = v.toString().trim(); + if (raw.isEmpty || raw.toLowerCase() == 'null') return '-'; + try { + final dateTime = DateTime.parse(raw); + return '${DateFormat('dd-MM-yyyy').format(dateTime)} ${DateFormat('HH:mm').format(dateTime)}'; + } catch (_) { + return raw; + } + } + + String _displayText(dynamic value) { + if (value == null) return '-'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '-'; + return text; + } + + String _truncateText(String value, {int maxChars = 25}) { + if (value.length <= maxChars) return value; + return '${value.substring(0, maxChars)}...'; + } + + String _partnerTooltipText(dynamic value) { + final text = _displayText(value); + if (text == '-') return text; + final items = text + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + if (items.isEmpty) return text; + return items.join('\n'); + } + + String _utrTooltipText(dynamic value) { + final text = _displayText(value); + if (text == '-') return text; + final items = text + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + if (items.isEmpty) return text; + return items.join('\n'); + } + + String _displayAmount(dynamic value) { + if (value == null) return '0'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '0'; + return text; + } + + double _toDouble(dynamic value) { + if (value == null) return 0; + return double.tryParse(value.toString()) ?? 0; + } + + String _generateUtrNumber() { + final now = DateTime.now(); + return 'UTR${DateFormat('yyyyMMddHHmmss').format(now)}'; + } + + Future _showAddPaymentDialog(Map item) async { + final TextEditingController amountController = TextEditingController(); + _paymentDate = DateTime.now(); + dynamic selectedUtrId; + List> utrDetails = []; + String generatedUtrNo = _generateUtrNumber(); + double totalAmount = _toDouble(item['invoiced_amount']); + double paidToDate = _toDouble(item['payout_amount']); + double currentBalance = _toDouble(item['balance_amount']); + double selectedUtrExistingPaid = 0; + double maxAllowedForSelectedUtr = 0; + + try { + final utrResponse = await apiService.getInvoiceUtrDetails(item['id']); + if (utrResponse['status'] == 'success') { + final dataRoot = utrResponse['data'] is Map + ? Map.from(utrResponse['data']) + : {}; + final invoiceSummary = + dataRoot['invoice'] is Map + ? Map.from(dataRoot['invoice']) + : {}; + totalAmount = _toDouble(invoiceSummary['total_amount']); + paidToDate = _toDouble(invoiceSummary['paid_to_date']); + currentBalance = _toDouble(invoiceSummary['balance_amount']); + utrDetails = List>.from(dataRoot['utrs_details'] ?? []); + if (utrDetails.isEmpty) { + generatedUtrNo = _generateUtrNumber(); + } + } + } catch (e) { + ToastHelper.showWarningToast( + context, + 'Failed to load UTR list. Please refresh and try again.', + ); + print('PL 8 => UTR load error: $e'); + } + + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + return StatefulBuilder( + builder: (ctx, setLocalState) { + return AlertDialog( + title: Text( + 'Add Paid Amount', + style: GoogleFonts.poppins(fontSize: 16, fontWeight: FontWeight.w600), + ), + content: SizedBox( + width: 360, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Invoice number #${_displayText(item['invoice_no'])}', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Text( + 'Total Amount', + style: GoogleFonts.inter(fontSize: 12), + ), + ), + Text(': ${totalAmount.toStringAsFixed(2)}', + style: GoogleFonts.inter(fontSize: 12)), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: Text( + 'Amount Paid Till Date', + style: GoogleFonts.inter(fontSize: 12), + ), + ), + Text(': ${paidToDate.toStringAsFixed(2)}', + style: GoogleFonts.inter(fontSize: 12)), + ], + ), + const SizedBox(height: 8), + const Divider(thickness: 1), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + 'Balance Amount', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + Text( + ': ${currentBalance.toStringAsFixed(2)}', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + if (currentBalance > 0) ...[ + if (utrDetails.isNotEmpty) + DropdownButtonFormField( + initialValue: selectedUtrId, + isExpanded: true, + decoration: const InputDecoration( + labelText: 'UTR Number', + border: OutlineInputBorder(), + ), + items: utrDetails.map((utr) { + return DropdownMenuItem( + value: utr['id'], + child: Text(_displayText(utr['utr_no'])), + ); + }).toList(), + onChanged: (value) { + final selected = utrDetails.firstWhere( + (row) => '${row['id']}' == '$value', + orElse: () => {}, + ); + final selectedPaid = _toDouble(selected['paid_amount']); + final selectedDate = selected['utr_date']; + setLocalState(() { + selectedUtrId = value; + selectedUtrExistingPaid = selectedPaid; + maxAllowedForSelectedUtr = + currentBalance + selectedUtrExistingPaid; + amountController.text = selectedPaid > 0 + ? selectedPaid.toStringAsFixed(2) + : ''; + if (selectedDate != null && + selectedDate.toString().trim().isNotEmpty) { + _paymentDate = + DateTime.tryParse(selectedDate.toString()) ?? + _paymentDate; + } + }); + }, + ) + else + TextFormField( + readOnly: true, + initialValue: generatedUtrNo, + decoration: const InputDecoration( + labelText: 'Generated UTR Number', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: 'Paid Amount', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + readOnly: true, + controller: TextEditingController( + text: DateFormat('dd-MM-yyyy').format(_paymentDate), + ), + decoration: const InputDecoration( + labelText: 'Paid Date', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.calendar_today_outlined, size: 18), + ), + onTap: () async { + final now = DateTime.now(); + final minDate = DateTime(now.year, now.month, now.day); + final picked = await showDatePicker( + context: ctx, + initialDate: _paymentDate.isBefore(minDate) + ? minDate + : _paymentDate, + firstDate: minDate, + lastDate: DateTime(2100), + ); + if (picked != null) { + setLocalState(() { + _paymentDate = picked; + }); + } + }, + ), + ] else + Text( + 'Invoice fully paid. No payment entry required.', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: const Color(0xFF2E7D6E), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancel'), + ), + if (currentBalance > 0) + ElevatedButton( + onPressed: () async { + final enteredAmount = _toDouble(amountController.text.trim()); + if (enteredAmount <= 0) { + ToastHelper.showWarningToast(context, 'Enter valid paid amount'); + return; + } + if (utrDetails.isNotEmpty && selectedUtrId == null) { + ToastHelper.showWarningToast(context, 'Please choose the UTR number'); + return; + } + if (enteredAmount < selectedUtrExistingPaid) { + ToastHelper.showWarningToast( + context, + 'Paid amount cannot be less than existing paid amount', + ); + return; + } + final maxAllowed = maxAllowedForSelectedUtr > 0 + ? maxAllowedForSelectedUtr + : currentBalance; + if (enteredAmount > maxAllowed) { + ToastHelper.showWarningToast( + context, + 'Paid amount cannot exceed balance amount', + ); + return; + } + Navigator.pop(ctx, true); + }, + child: const Text('Save'), + ), + ], + ); + }, + ); + }, + ); + + if (result != true) return; + + try { + setState(() => isLoading = true); + final response = await apiService.updateInvoiceUtrDetails({ + 'invoice_id': int.tryParse(item['id'].toString()) ?? 0, + 'paid_amount': _toDouble(amountController.text.trim()), + 'utr_id': selectedUtrId != null ? int.tryParse('$selectedUtrId') : null, + 'utr_no': selectedUtrId == null ? generatedUtrNo : null, + 'paid_date': DateFormat('yyyy-MM-dd').format(_paymentDate), + 'created_by': userId, + 'updated_by': userId, + }); + + if (response['status'] == 'success') { + ToastHelper.showSuccessToast(context, 'UTR details updated successfully'); + await getPayoutList(); + } else { + ToastHelper.showWarningToast( + context, + response['message']?.toString() ?? 'Failed to update UTR details', + ); + } + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + setState(() => isLoading = false); + } + } + + void _showAccountHistoryModal(Map item) { + showDialog( + context: context, + builder: (_) => AccountHistoryModal( + invoiceId: item['id'], + ), + ); + } + + @override + Widget build(BuildContext context) { + return MainLayout( + title: "Pay Out", + body: SelectionArea( + child: Container( + // height: 30, + // color: Colors.yellow.shade50, + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Container( + // height: 30, + // width: MediaQuery.of(context).size.width, + // child: GestureDetector( + // onTap: () { + // context.go(AppRoutes.dashboard); + // }, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // // Tooltip( + // // message: 'Back', + // // child: IconButton( + // // icon: const Icon( + // // Icons.arrow_left_sharp, + // // size: 25, + // // color: Color(0xFF425B5B), + // // ), + // // onPressed: () { + // // context.go(AppRoutes.dashboard); + // // }, + // // splashRadius: 18, + // // hoverColor: Colors.black12, + // // padding: const EdgeInsets.all(4), + // // constraints: const BoxConstraints(), + // // ), + // // ), + // // const SizedBox(width: 5), // spacing between icon and text + // Text( + // "Invoice", + // style: GoogleFonts.inter( + // fontSize: 14, + // fontWeight: FontWeight.w500, + // ), + // ), + // ], + // ), + // ), + // ), + // + // SizedBox(height: 5), + Expanded( + child: Container( + width: MediaQuery.of(context).size.width, + padding: EdgeInsets.all(8.0), + child: _buildContent(context), + ), + ), + + Container( + // height: 20, + width: MediaQuery.of(context).size.width, + // color: Colors.green.shade50, + child: PaginationControls( + currentPage: currentPage, + itemsPerPage: itemsPerPage, + // totalItems: dataVal.length, + totalItems: filteredData.length, + // activeColor: layoutColor, // your theme color + onPageChanged: (page) { + setState(() { + currentPage = page; + }); + }, + onItemsPerPageChanged: (items) { + setState(() { + itemsPerPage = items; + currentPage = 1; + }); + }, + ), + ), + ], + ), + ) ,) , + ); + } + + Widget _buildContent(BuildContext context) { + return Column( + children: [ + Container( + // height: 40, + // color: Colors.pink, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + "Payout", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + + Spacer(), + ThemedSearchField( + hintText: 'Search', + // backgroundColor: Color(0xFFF6F8F8), + onChanged: filterData, + controller: _searchStaffController, + txtHeight: 30, + txtwidth: ResponsiveLayout.isMobile(context) + ? MediaQuery.of(context).size.width * 0.7 + : MediaQuery.of(context).size.width * 0.15, + ), + SizedBox(width: 10), + Tooltip( + message: 'Add Payout Details', + child: InkWell( + onTap: () async { + context.go(AppRoutes.payoutDetails); + }, + child: Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: const Color(0xFF2E7D6E), + // color: const Color(0xFF425B5B), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Image.asset( + // "assets/miscellaneous/export.png", + // height: 25, + // width: 25, + // ), + Icon(Icons.add, color: Colors.white, size: 18), + // Image.asset("assets/miscellaneous/export", height: 15, width: 15), + ], + ), + ), + ), + ), + SizedBox(width: 10), + ExportBtn( + sheetName: "Payout", + fileName: "Payout_List", + data: filteredData, + txt: true, + displayHeaders: [ + "S.No", + "Invoice No", + "Invoice Date", + "Broker Name", + "Referer", + "UTR Details", + "Invoiced Amount", + "Payout Amount", + "Balance Amount", + "Status", + "Updated Date", + ], + keys: [ + "id", + "invoice_no", + "invoice_date", + "broker_name", + "partner_names", + "utr_numbers", + "invoiced_amount", + "payout_amount", + "balance_amount", + "payout_status", + "updated_at", + ], + valueFormatters: { + 'payout_status': (v, _) => _exportPayoutStatusLabel(v), + 'updated_at': (v, _) => _exportUpdatedAtForExcel(v), + }, + ), + ], + ), + ), + SizedBox(height: 5), + Container( + decoration: BoxDecoration( + color: Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + Expanded(flex: 1, child: Text('S.No', style: _headerStyle)), + Expanded( + flex: 2, + child: Text('Invoice Date', style: _headerStyle), + ), + Expanded( + flex: 3, + child: Text('Invoice No', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Broker Name', style: _headerStyle), + ), + Expanded(flex: 3, child: Text('Referer', style: _headerStyle)), + Expanded(flex: 2, child: Text('UTR Details', style: _headerStyle)), + Expanded( + flex: 2, + child: Text('Invoiced\nAmount', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Payout\nAmount', style: _headerStyle), + ), + Expanded( + flex: 2, + child: Text('Balance\nAmount', style: _headerStyle), + ), + Expanded(flex: 2, child: Text('Status', style: _headerStyle)), + Expanded( + flex: 2, + child: Text('Updated Date', style: _headerStyle), + ), + Expanded(flex: 2, child: Text('Action', style: _headerStyle)), + ], + ), + ), + Expanded(child: _buildDataTable(context)), + ], + ); + } + + Widget _buildDataTable(BuildContext context) { + if (filteredData.isEmpty) { + return const SizedBox( + height: 50, + child: Center(child: Text('No available data')), + ); + } + + final sortedData = [..._paginatedData]; + + // Desktop: keep ListView.builder + return ListView.builder( + itemCount: sortedData.length + 1, + itemBuilder: (context, index) { + if (index == 0) return _buildHeader(); + final startIndex = (currentPage - 1) * itemsPerPage; + final item = sortedData[index - 1]; + final sno = startIndex + index; + return _buildDataRow(item, sno); + }, + ); + } + + Widget _buildHeader() { + return SizedBox.shrink(); + } + + Widget _buildDataRow(Map item, sno) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16), + // margin: const EdgeInsets.only(top: 10), + decoration: BoxDecoration( + color: Colors.white, + // color: Color(0xFFE0F7F9), + border: const Border( + bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1), + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: Text('$sno', style: _dataBold), + ), + Expanded( + flex: 2, + child: Text( + item['invoice_date'] != null + ? _formatDate(item['invoice_date']) + : '-', + style: _dataBold, + ), + ), + Expanded( + flex: 3, + child: Text( + _displayText(item['invoice_no']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 2, + child: Text( + _displayText(item['broker_name']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 3, + child: Tooltip( + message: _partnerTooltipText(item['partner_names']), + waitDuration: const Duration(milliseconds: 250), + child: Text( + _truncateText(_displayText(item['partner_names']), maxChars: 25), + style: _dataBold, + softWrap: false, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + Expanded( + flex: 2, + child: Tooltip( + message: _utrTooltipText(item['utr_numbers']), + waitDuration: const Duration(milliseconds: 250), + child: Text( + _truncateText(_displayText(item['utr_numbers']), maxChars: 10), + style: _dataBold, + softWrap: false, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + _displayAmount(item['invoiced_amount']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 2, + child: Text( + _displayAmount(item['payout_amount']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 2, + child: Text( + _displayAmount(item['balance_amount']), + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + Expanded( + flex: 2, + child: Text( + // item['payout_status'] != null ? item['payout_status'] : '-', + item['payout_status'] == '1' + ? 'Pending' + : item['payout_status'] != null + ? 'Completed' + : '-', + style: _dataBold, + softWrap: true, + maxLines: 3, + ), + ), + + Expanded( + flex: 2, + child: item['updated_at'] != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatDate(item['updated_at']), + style: _dataBold, + softWrap: false, + maxLines: 1, + ), + Text( + _formatTime(item['updated_at']), + style: _dataBold, + softWrap: false, + maxLines: 1, + ), + ], + ) + : Text('-', style: _dataBold), + ), + + // ACTION ICONS (Edit + Delete) + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Step 1: Open payout/view screen for selected invoice. + IconButton( + icon: const Icon( + Icons.remove_red_eye_outlined, + color: Colors.blue, + size: _actionIconSize, + ), + tooltip: 'View Payout', + onPressed: () { + // TODO: handle edit + context.go(AppRoutes.payoutDetails, extra: item); + print("PL 9 => Edit clicked for ${item['invoice_no']}"); + }, + splashRadius: 16, + hoverColor: Colors.black12, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, + ), + const SizedBox(width: _actionIconGap), + + // Step 2: Add payment only when balance amount is pending. + IconButton( + icon: const Icon( + Icons.currency_rupee, + color: Colors.green, + size: _actionIconSize, + ), + tooltip: 'Add payment', + onPressed: () { + final balance = _toDouble(item['balance_amount']); + if (balance <= 0) { + ToastHelper.showWarningToast(context, 'Invoice already fully paid'); + return; + } + _showAddPaymentDialog(item); + }, + splashRadius: 16, + hoverColor: Colors.black12, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, + ), + const SizedBox(width: _actionIconGap), + + // Step 3: Show account history modal for this invoice. + IconButton( + icon: const Icon( + Icons.history, + color: Colors.green, + size: _actionIconSize, + ), + tooltip: 'Account details', + onPressed: () => _showAccountHistoryModal(item), + splashRadius: 16, + hoverColor: Colors.black12, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, + ), + const SizedBox(width: _actionIconGap), + + // Step 4: Delete invoice entry. + IconButton( + icon: const Icon(Icons.delete, color: Colors.red, size: _actionIconSize), + tooltip: 'Delete Payout', + onPressed: () { + // TODO: handle delete + deleteInvoice(item['id']); + print("PL 10 => Delete clicked for ${item['invoice_no']}"); + }, + splashRadius: 16, + hoverColor: Colors.black12, + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: _actionButtonSize, + height: _actionButtonSize, + ), + visualDensity: VisualDensity.compact, + ), + ], + ), + ), + ], + ), + ); + } + + static final _dataBold = GoogleFonts.inter( + fontSize: 11.5, + fontWeight: FontWeight.w400, + color: Color(0xFF000000), + ); + + static final _headerStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: Color(0xFF1E293B), + ); +} + +class AccountHistoryModal extends StatefulWidget { + final dynamic invoiceId; + const AccountHistoryModal({super.key, required this.invoiceId}); + + @override + State createState() => _AccountHistoryModalState(); +} + +class _AccountHistoryModalState extends State { + late ApiService apiService; + bool isLoading = true; + List> accountHistoryData = []; + Map? invoiceSummary; + final TextStyle _historyHeaderStyle = GoogleFonts.poppins( + fontSize: 11.2, + fontWeight: FontWeight.w500, + color: const Color(0xFF1E293B), + ); + final TextStyle _historyCellStyle = GoogleFonts.inter( + fontSize: 11.5, + fontWeight: FontWeight.w400, + color: const Color(0xFF000000), + ); + + String _formatINR(dynamic value) { + final amount = double.tryParse(value?.toString() ?? '') ?? 0; + return NumberFormat.currency( + locale: 'en_IN', + symbol: '₹', + decimalDigits: 0, + ).format(amount); + } + + String _formatTimelineDate(dynamic rawDate) { + if (rawDate == null) return '-'; + try { + final dt = DateTime.parse(rawDate.toString()); + return DateFormat('d MMM yyyy').format(dt); + } catch (_) { + return rawDate.toString(); + } + } + + double _toDouble(dynamic value) { + if (value == null) return 0; + return double.tryParse(value.toString()) ?? 0; + } + + Widget _buildWebActivityBody() { + // Step 1: Sort payments by paid date so we can take "latest" balance. + final sortedPayments = [...accountHistoryData]..sort((a, b) { + DateTime da = DateTime.tryParse((a['paid_date'] ?? '').toString()) ?? + DateTime.fromMillisecondsSinceEpoch(0); + DateTime db = DateTime.tryParse((b['paid_date'] ?? '').toString()) ?? + DateTime.fromMillisecondsSinceEpoch(0); + return da.compareTo(db); + }); + + final latestBalance = sortedPayments.isEmpty + ? 0.0 + : _toDouble(sortedPayments.last['balance_amount']); + final paidToDate = sortedPayments.fold( + 0.0, + (sum, item) => sum + _toDouble(item['paid_amount']), + ); + final totalAmount = invoiceSummary != null + ? _toDouble(invoiceSummary?['total_amount']) + : paidToDate + latestBalance; + + // Step 2: Build UI sections (responsive and cleaner typography). + final invoiceNo = _displayText(invoiceSummary?['invoice_no']); + final invoiceLabel = + invoiceNo.startsWith('#') ? invoiceNo : '#$invoiceNo'; + final width = MediaQuery.of(context).size.width; + final bool isCompact = width < 1200; + final double titleFont = isCompact ? 17 : 20; + final double amountBigFont = isCompact ? 22 : 26; + final double sectionTitleFont = isCompact ? 12 : 14; + final double bodyFont = isCompact ? 11 : 12; + final double valueFont = isCompact ? 14 : 16; + + return Padding( + padding: EdgeInsets.all(isCompact ? 10 : 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Step 2a: Top row (Invoice ID only). + Row( + children: [ + Text( + invoiceLabel, + style: GoogleFonts.inter( + fontSize: titleFont, + fontWeight: FontWeight.w800, + color: const Color(0xFF0F172A), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Step 2b: Total amount card. + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'TOTAL AMOUNT', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w600, + letterSpacing: 0.2, + color: const Color(0xFF334155), + ), + ), + const SizedBox(height: 6), + Text( + _formatINR(totalAmount), + style: GoogleFonts.inter( + fontSize: amountBigFont, + fontWeight: FontWeight.w900, + color: const Color(0xFF0B3A6B), + ), + ), + ], + ), + const Spacer(), + Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: const Color(0xFFEAF2FF), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.receipt_long_rounded, + color: Color(0xFF2563EB), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Step 2c: Payment timeline header. + Row( + children: [ + Text( + 'PAYMENT TIMELINE', + style: GoogleFonts.poppins( + fontSize: sectionTitleFont, + fontWeight: FontWeight.w700, + color: const Color(0xFF111827), + ), + ), + const Spacer(), + Text( + '${sortedPayments.length} TRANSACTIONS', + style: GoogleFonts.inter( + fontSize: bodyFont, + fontWeight: FontWeight.w600, + color: const Color(0xFF334155), + ), + ), + ], + ), + + const SizedBox(height: 10), + + // Step 2d: Timeline list. + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(top: 2), + itemCount: sortedPayments.length, + itemBuilder: (context, index) { + // Show latest first on the UI. + final data = + sortedPayments[sortedPayments.length - 1 - index]; + final name = _displayText(data['createdby_name']); + final paidAmount = _formatINR(data['paid_amount']); + final date = _formatTimelineDate(data['paid_date']); + + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Color(0xFFECFDF5), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check_circle, + size: 22, + color: Color(0xFF16A34A), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: GoogleFonts.inter( + fontSize: isCompact ? 12 : 13, + fontWeight: FontWeight.w700, + color: const Color(0xFF0F172A), + ), + ), + const SizedBox(height: 4), + Text( + date, + style: GoogleFonts.inter( + fontSize: bodyFont, + fontWeight: FontWeight.w500, + color: const Color(0xFF475569), + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + paidAmount, + style: GoogleFonts.inter( + fontSize: isCompact ? 14 : 15, + fontWeight: FontWeight.w800, + color: const Color(0xFF0B3A6B), + ), + ), + ], + ), + ], + ), + ); + }, + ), + ), + + const SizedBox(height: 12), + + // Step 2e: Bottom summary. + Row( + children: [ + Expanded( + child: Text( + 'TOTAL AMOUNT', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w700, + color: const Color(0xFF334155), + ), + ), + ), + Text( + _formatINR(totalAmount), + style: GoogleFonts.inter( + fontSize: valueFont, + fontWeight: FontWeight.w900, + color: const Color(0xFF0F172A), + ), + ), + ], + ), + + const SizedBox(height: 8), + + Row( + children: [ + Expanded( + child: Text( + 'PAID TO DATE', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w700, + color: const Color(0xFF334155), + ), + ), + ), + Text( + _formatINR(paidToDate), + style: GoogleFonts.inter( + fontSize: valueFont, + fontWeight: FontWeight.w900, + color: const Color(0xFF0F172A), + ), + ), + ], + ), + + const SizedBox(height: 10), + + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + decoration: BoxDecoration( + color: const Color(0xFFBFDBFE).withOpacity(0.45), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Expanded( + child: Text( + 'BALANCE', + style: GoogleFonts.poppins( + fontSize: bodyFont, + fontWeight: FontWeight.w800, + color: const Color(0xFF0F172A), + ), + ), + ), + Text( + _formatINR(latestBalance), + style: GoogleFonts.inter( + fontSize: isCompact ? 16 : 18, + fontWeight: FontWeight.w900, + color: const Color(0xFF0B3A6B), + ), + ), + ], + ), + ), + ], + ), + ); + } + + @override + void initState() { + super.initState(); + apiService = ApiService(); + fetchAccountHistory(); + } + + String _displayText(dynamic value) { + if (value == null) return '-'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '-'; + return text; + } + + String _displayAmount(dynamic value) { + if (value == null) return '0.00'; + final parsed = double.tryParse(value.toString()) ?? 0; + return parsed.toStringAsFixed(2); + } + + String _formatIndianDate(dynamic rawDate) { + if (rawDate == null) return '-'; + try { + final dateTime = DateTime.parse(rawDate.toString()); + return DateFormat('dd-MM-yyyy hh:mm a').format(dateTime); + } catch (_) { + return rawDate.toString(); + } + } + + DateTime _parseDateTime(dynamic rawDate) { + if (rawDate == null) { + return DateTime.fromMillisecondsSinceEpoch(0); + } + return DateTime.tryParse(rawDate.toString()) ?? + DateTime.fromMillisecondsSinceEpoch(0); + } + + DateTime _rowEffectiveDate(Map row) { + final paidAt = _parseDateTime(row['paid_date']); + if (paidAt.millisecondsSinceEpoch > 0) return paidAt; + final createdAt = _parseDateTime(row['created_at']); + if (createdAt.millisecondsSinceEpoch > 0) return createdAt; + return _parseDateTime(row['updated_at']); + } + + String _formatPaidDateTime(Map row) { + final paidAt = _parseDateTime(row['paid_date']); + if (paidAt.millisecondsSinceEpoch > 0 && paidAt.hour + paidAt.minute > 0) { + return DateFormat('dd-MM-yyyy hh:mm a').format(paidAt); + } + final createdAt = _parseDateTime(row['created_at']); + if (createdAt.millisecondsSinceEpoch > 0) { + return DateFormat('dd-MM-yyyy hh:mm a').format(createdAt); + } + if (paidAt.millisecondsSinceEpoch > 0) { + return DateFormat('dd-MM-yyyy').format(paidAt); + } + return '-'; + } + + List> _buildMeaningfulHistoryRows() { + final sortedRows = [...accountHistoryData]..sort((a, b) { + final dateCompare = _rowEffectiveDate(a).compareTo(_rowEffectiveDate(b)); + if (dateCompare != 0) return dateCompare; + final idA = int.tryParse('${a['id'] ?? 0}') ?? 0; + final idB = int.tryParse('${b['id'] ?? 0}') ?? 0; + return idA.compareTo(idB); + }); + + double runningPaid = 0; + final rowsWithMetrics = sortedRows.map((row) { + final paid = _toDouble(row['paid_amount']); + runningPaid += paid; + return { + ...row, + 'total_paid_till_date': runningPaid, + }; + }).toList(); + + return rowsWithMetrics.reversed.toList(); + } + + Future fetchAccountHistory() async { + setState(() => isLoading = true); + try { + final response = await apiService.listInvoiceUtrDetails(widget.invoiceId); + if (response['status'] == 'success') { + setState(() { + accountHistoryData = List>.from( + response['data'] ?? [], + ); + invoiceSummary = response['invoice'] is Map + ? Map.from(response['invoice']) + : null; + }); + } else { + setState(() { + accountHistoryData = []; + invoiceSummary = null; + }); + } + } catch (_) { + setState(() { + accountHistoryData = []; + invoiceSummary = null; + }); + } finally { + if (mounted) setState(() => isLoading = false); + } + } + + String _safeFileNamePart(String value) { + return value.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim(); + } + + Future _exportAccountHistoryExcel() async { + if (accountHistoryData.isEmpty) { + ToastHelper.showWarningToast(context, 'No account details data to export'); + return; + } + + final invoiceNo = _displayText(invoiceSummary?['invoice_no']); + final cleanedInvoiceNo = _safeFileNamePart( + invoiceNo == '-' ? 'Invoice number' : invoiceNo, + ); + + final exportRows = _buildMeaningfulHistoryRows(); + // ExcelExporter internally reverses incoming rows before writing. + // So we pass oldest->latest to get latest->oldest in the final file. + final exportInputRows = exportRows.reversed.toList(); + final exportData = exportInputRows.asMap().entries.map((entry) { + final index = entry.key; + final row = entry.value; + return { + 's_no': index + 1, + 'invoice_no': _displayText(row['invoice_no']), + 'utr_no': _displayText(row['utr_no']), + 'total_amount': _displayAmount(row['total_amount']), + 'paid_date_time': _formatPaidDateTime(row), + 'createdby_name': _displayText(row['createdby_name']), + 'paid_amount': _displayAmount(row['paid_amount']), + 'total_paid_till_date': _displayAmount(row['total_paid_till_date']), + 'balance_amount': _displayAmount(row['balance_amount']), + }; + }).toList(); + + await ExcelExporter.exportToExcel( + sheetName: 'Account Details', + fileName: 'Account details $cleanedInvoiceNo', + data: exportData, + displayHeaders: const [ + 'S.No', + 'Invoice Number', + 'UTR Number', + 'Total Amount', + 'Paid Date & Time', + 'Updated By', + 'Paid Amount', + 'Total Paid Till Date', + 'Balance', + ], + keys: const [ + 's_no', + 'invoice_no', + 'utr_no', + 'total_amount', + 'paid_date_time', + 'createdby_name', + 'paid_amount', + 'total_paid_till_date', + 'balance_amount', + ], + ); + } + + @override + Widget build(BuildContext context) { + final bool isMobile = ResponsiveLayout.isMobile(context); + final media = MediaQuery.of(context).size; + + return Dialog( + insetPadding: EdgeInsets.symmetric( + horizontal: isMobile ? 12 : 24, + vertical: isMobile ? 18 : 24, + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isMobile ? media.width : 1100, + maxHeight: media.height * (isMobile ? 0.86 : 0.82), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: const BoxDecoration( + color: Color(0xFFF8FAFC), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), + ), + border: Border(bottom: BorderSide(color: Color(0xFFE5E7EB))), + ), + child: Row( + children: [ + Text( + 'Account Details', + style: GoogleFonts.poppins( + fontSize: 14, + color: const Color(0xFF2E7D6E), + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: isLoading ? null : _exportAccountHistoryExcel, + icon: const Icon(Icons.file_download_outlined, size: 18), + label: const Text('Export Excel'), + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF2E7D6E), + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon( + Icons.close, + size: 18, + color: Color(0xFF2E7D6E), + ), + splashRadius: 18, + ), + ], + ), + ), + Expanded( + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : accountHistoryData.isEmpty + ? const Center(child: Text('No Account Details Data Found')) + : Padding( + padding: const EdgeInsets.all(12), + child: Container( + color: Colors.white, + child: LayoutBuilder( + builder: (context, constraints) { + final historyRows = _buildMeaningfulHistoryRows(); + return Scrollbar( + thumbVisibility: true, + child: SingleChildScrollView( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.maxWidth, + ), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + const Color(0xFFF1F5F9), + ), + dataRowMinHeight: 42, + dataRowMaxHeight: 52, + columnSpacing: isMobile ? 14 : 20, + columns: [ + DataColumn( + label: Text('S.No', style: _historyHeaderStyle), + ), + DataColumn( + label: Text( + 'Invoice Number', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'Total Amount', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'UTR Number', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'Paid Date & Time', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text('Updated By', style: _historyHeaderStyle), + ), + DataColumn( + label: Text( + 'Paid Amount', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text( + 'Total Paid Till Date', + style: _historyHeaderStyle, + ), + ), + DataColumn( + label: Text('Balance', style: _historyHeaderStyle), + ), + ], + rows: List.generate(historyRows.length, (index) { + final data = historyRows[index]; + return DataRow( + cells: [ + DataCell( + Text('${index + 1}', style: _historyCellStyle), + ), + DataCell( + Text( + _displayText(data['invoice_no']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['total_amount']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayText(data['utr_no']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _formatPaidDateTime(data), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayText(data['createdby_name']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['paid_amount']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['total_paid_till_date']), + style: _historyCellStyle, + ), + ), + DataCell( + Text( + _displayAmount(data['balance_amount']), + style: _historyCellStyle, + ), + ), + ], + ); + }), + ), + ), + ), + ), + ); + }, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/screens/payout/payout_screen.dart b/lib/presentation/screens/payout/payout_screen.dart index 6867338..bc80ff2 100644 --- a/lib/presentation/screens/payout/payout_screen.dart +++ b/lib/presentation/screens/payout/payout_screen.dart @@ -1,7 +1,11 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:dropdown_search/dropdown_search.dart'; +import 'package:excel/excel.dart' hide Border; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -29,6 +33,9 @@ class PayOutScreen extends ConsumerStatefulWidget { } class _PayOutScreenState extends ConsumerState { + static final List _commissionInputFormatter = [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), + ]; // -------------------------- // Invoice Fields // -------------------------- @@ -94,6 +101,8 @@ class _PayOutScreenState extends ConsumerState { bool isLoading = false; bool isLoadingPOS = false; bool isLoadingEditData = false; + bool hasFetchedTableData = false; + final Map _uploadedUtrByPolicyKey = {}; late ApiService apiService; dynamic managerId; dynamic userId; @@ -230,6 +239,8 @@ class _PayOutScreenState extends ConsumerState { selectedPolicies = {}; totalPolicies = ''; totalCommission = ''; + filteredPolicies = []; + hasFetchedTableData = false; // dropDownKeyPartner.currentState?.clear(); }); } @@ -474,6 +485,10 @@ class _PayOutScreenState extends ConsumerState { Future loadPolicies() async { print('loadPolicies'); print("H13 => r : $roleId | mId: $managerId | uId: $userId "); + setState(() { + isLoading = true; + hasFetchedTableData = false; + }); try { final List agentIds = (selectedAgentId ?? []) .map((e) => int.parse(e)) @@ -494,6 +509,7 @@ class _PayOutScreenState extends ConsumerState { print('API Data response - $response'); setState(() { filteredPolicies = List>.from(response['data']); + hasFetchedTableData = true; print('API Data - $filteredPolicies'); // filteredPolicies = allPolicies.where((p) { // if (p["agentId"] != selectedAgentId) return false; @@ -504,10 +520,17 @@ class _PayOutScreenState extends ConsumerState { // }).toList(); }); } else { - filteredPolicies = []; + setState(() { + filteredPolicies = []; + hasFetchedTableData = true; + }); } } catch (e) { print('Exception occurred: $e'); + setState(() { + filteredPolicies = []; + hasFetchedTableData = false; + }); } finally { setState(() { isLoading = false; @@ -540,7 +563,11 @@ class _PayOutScreenState extends ConsumerState { return; } - if (selectedPOS == null) { + final bool isNhanceBroker = _isNhanceBrokerSelected(); + final bool hasValidPosSelection = + selectedPOS != null && selectedPOS.toString().trim().isNotEmpty; + + if (isNhanceBroker && !hasValidPosSelection) { ToastHelper.showWarningToast(context, "Please select POS"); return; } @@ -564,7 +591,7 @@ class _PayOutScreenState extends ConsumerState { // Total Commission final double totalCommission = filteredPolicies .where((p) => selectedPolicies.contains(int.parse(p["policy_id"]))) - .fold(0.0, (sum, p) => sum + double.parse(p["commission_amount"])); + .fold(0.0, (sum, p) => sum + _parseCommissionValue(p["commission_amount"])); // Build items array final List> items = filteredPolicies @@ -573,10 +600,13 @@ class _PayOutScreenState extends ConsumerState { (p) => { "policy_id": int.parse(p["policy_id"]), "policy_no": p["policy_no"], - "commission_amount": double.parse(p["commission_amount"]), + "commission_amount": _parseCommissionValue(p["commission_amount"]), }, ) .toList(); + final List> utrs = _buildUtrPayloadForSelectedPolicies( + defaultUtrDate: formattedInvoiceDate, + ); final jsondata = { "invoice_no": controllers['invoiceNo']?.text, @@ -586,10 +616,11 @@ class _PayOutScreenState extends ConsumerState { "invoice_date": formattedInvoiceDate, "till_date": formattedTillDate, "payout_status": 0, - "pos_id": selectedPOS, + "pos_id": isNhanceBroker ? selectedPOS : 0, "created_by": userId, "updated_by": userId, "items": items, + "utrs": utrs, }; print('Commissions $jsondata'); final bool confirm = await showSaveConfirmation(context); @@ -605,6 +636,7 @@ class _PayOutScreenState extends ConsumerState { invoiceDate = DateTime.now(); policyTillDate = DateTime.now(); filteredPolicies = []; + _uploadedUtrByPolicyKey.clear(); }); context.go(AppRoutes.invoiceList); } else { @@ -619,6 +651,547 @@ class _PayOutScreenState extends ConsumerState { } } + bool _isNhanceBrokerSelected() { + final selectedBroker = getBrokerData.firstWhere( + (item) => item['id'].toString() == (selectedBrokerID ?? '').toString(), + orElse: () => {}, + ); + + if (selectedBroker.isEmpty) return false; + + final brokerName = (selectedBroker['name'] ?? '').toString().toLowerCase(); + return brokerName.contains('nhance'); + } + + String _normalizeHeader(dynamic header) { + return header + .toString() + .trim() + .toLowerCase() + .replaceAll(RegExp(r'[^a-z0-9]'), ''); + } + + String _excelCellValue(Data? cell) { + final value = cell?.value; + if (value == null) return ''; + return value.toString().trim(); + } + + String _normalizePolicyNo(dynamic value) { + return (value ?? '') + .toString() + .trim() + .toLowerCase() + .replaceAll(RegExp(r'\s+'), ''); + } + + String _displayText(dynamic value) { + if (value == null) return '-'; + final text = value.toString().trim(); + if (text.isEmpty || text.toLowerCase() == 'null') return '-'; + return text; + } + + double? _parseExcelCommission(String raw) { + final cleaned = raw + .replaceAll('₹', '') + .replaceAll(',', '') + .replaceAll(RegExp(r'\s+'), '') + .trim(); + if (cleaned.isEmpty) return null; + return double.tryParse(cleaned); + } + + List> _buildUtrPayloadForSelectedPolicies({ + required String defaultUtrDate, + }) { + /* + * Build UTR payload from selected policies. + * 1) UTR source is Excel "UTR Number" column captured per policy. + * 2) If same UTR appears in multiple selected rows, amount is aggregated. + * 3) Amount comes from selected policy commission amount. + */ + final Map utrAmountMap = {}; + + for (final policy in filteredPolicies) { + final int policyId = int.tryParse(policy["policy_id"]?.toString() ?? "0") ?? 0; + if (!selectedPolicies.contains(policyId)) continue; + + final String policyKey = _normalizePolicyNo(policy['policy_no']); + final String utrNo = (_uploadedUtrByPolicyKey[policyKey] ?? '').trim(); + if (utrNo.isEmpty) continue; + + final double amount = _parseCommissionValue(policy["commission_amount"]); + utrAmountMap[utrNo] = (utrAmountMap[utrNo] ?? 0) + amount; + } + + return utrAmountMap.entries + .map( + (e) => { + "utr_no": e.key, + "amount": e.value, + "utr_date": defaultUtrDate, + }, + ) + .toList(); + } + + Widget _excelMatchedPolicyTile(Map row) { + final givenUtr = (row['given_utr_no'] ?? '').trim(); + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE5E7EB)), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Policy number: ${row['policy_no']}'), + const SizedBox(height: 4), + Text('Current commission: ${row['current_commission']}'), + const SizedBox(height: 4), + Text('Excel commission: ${row['excel_commission']}'), + if (givenUtr.isNotEmpty) ...[ + const SizedBox(height: 4), + Text('UTR number: $givenUtr'), + ], + ], + ), + ); + } + + /// No Excel row matched the table — list all Excel policy numbers; OK only. + Future _showAllPoliciesUnmatchedDialog({ + required List excelPolicyNumbersNotMatched, + required int invalidExcelRows, + }) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: Text( + 'No policies matched', + style: GoogleFonts.inter(fontWeight: FontWeight.w600), + ), + content: SizedBox( + width: 520, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'None of the policy numbers in the Excel file match the policies in the current table.', + style: GoogleFonts.inter(fontSize: 13, height: 1.35), + ), + if (excelPolicyNumbersNotMatched.isNotEmpty) ...[ + const SizedBox(height: 12), + Text( + 'Policy numbers from Excel (not matched):', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + ...excelPolicyNumbersNotMatched.map( + (p) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text('• $p', style: GoogleFonts.inter(fontSize: 13)), + ), + ), + ], + if (invalidExcelRows > 0) ...[ + const SizedBox(height: 12), + Text( + '$invalidExcelRows row(s) in Excel were skipped (invalid policy number or commission).', + style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]), + ), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('OK', style: GoogleFonts.inter(fontWeight: FontWeight.w600)), + ), + ], + ), + ); + } + + /// At least one match — show matched one-by-one; if Excel has extra policies, list them; Proceed / Cancel. + Future _showUploadReviewProceedDialog({ + required List> matchedRows, + required List excelPolicyNumbersNotOnTable, + required int invalidExcelRows, + }) async { + final hasExcelOnly = excelPolicyNumbersNotOnTable.isNotEmpty; + return await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: Text( + hasExcelOnly ? 'Review Excel vs table' : 'Confirm commission update', + style: GoogleFonts.inter(fontWeight: FontWeight.w600), + ), + content: SizedBox( + width: 560, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Matched policies (${matchedRows.length})', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + ...matchedRows.map(_excelMatchedPolicyTile), + if (hasExcelOnly) ...[ + const SizedBox(height: 16), + Text( + 'Not found on current table (${excelPolicyNumbersNotOnTable.length})', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.orange.shade800, + ), + ), + const SizedBox(height: 6), + Text( + 'These policy numbers are in Excel but not in the loaded table:', + style: GoogleFonts.inter(fontSize: 12, height: 1.35), + ), + const SizedBox(height: 8), + ...excelPolicyNumbersNotOnTable.map( + (p) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( + '• $p', + style: GoogleFonts.inter(fontSize: 13), + ), + ), + ), + ], + if (invalidExcelRows > 0) ...[ + const SizedBox(height: 12), + Text( + 'Note: $invalidExcelRows Excel row(s) skipped (invalid data).', + style: GoogleFonts.inter(fontSize: 12, color: Colors.grey[700]), + ), + ], + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: Text('Cancel', style: GoogleFonts.inter()), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text('Proceed', style: GoogleFonts.inter()), + ), + ], + ), + ) ?? + false; + } + + Future _showMismatchProceedDialog(int mismatchCount) async { + return await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('Partner mismatch found'), + content: Text( + '$mismatchCount row(s) are linked to a different partner.\n' + 'Proceeding will retroactively update partner mapping in payout records.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Proceed'), + ), + ], + ), + ) ?? + false; + } + + Future _triggerBulkUpload() async { + final brokerId = int.tryParse( + (selectedBrokerID ?? '').toString().trim(), + ); + if (brokerId == null) { + ToastHelper.showWarningToast( + context, + 'Please select a broker before uploading.', + ); + return; + } + if (selectedAgentId == null || selectedAgentId!.isEmpty) { + ToastHelper.showWarningToast( + context, + 'Please select Referrer before uploading.', + ); + return; + } + if (!hasFetchedTableData || filteredPolicies.isEmpty) { + ToastHelper.showWarningToast( + context, + 'No policy data loaded. Set dates, Referrer, broker, tap Filter, then upload.', + ); + return; + } + + try { + final picked = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xlsx', 'xls'], + withData: true, + ); + if (picked == null || picked.files.isEmpty) { + ToastHelper.showWarningToast( + context, + 'No Excel file selected. Please choose a file to upload.', + ); + return; + } + final selectedFile = picked.files.single; + if (selectedFile.bytes == null || selectedFile.bytes!.isEmpty) { + ToastHelper.showWarningToast( + context, + 'Unable to read the selected Excel file. Try another file.', + ); + return; + } + + setState(() => isLoading = true); + final excel = Excel.decodeBytes(Uint8List.fromList(selectedFile.bytes!)); + if (excel.tables.isEmpty) { + ToastHelper.showWarningToast( + context, + 'Invalid Excel: no sheet found in the file.', + ); + return; + } + + final sheet = excel.tables.values.first; + final rows = sheet.rows; + if (rows.isEmpty) { + ToastHelper.showWarningToast( + context, + 'Uploaded Excel file is empty.', + ); + return; + } + + final headerRow = rows.first; + int policyNoCol = -1; + int commissionCol = -1; + int utrNoCol = -1; + + for (int i = 0; i < headerRow.length; i++) { + final normalized = _normalizeHeader(_excelCellValue(headerRow[i])); + if (normalized == 'policynumber') policyNoCol = i; + if (normalized == 'commissionamount') commissionCol = i; + if (normalized == 'utrnumber') utrNoCol = i; + } + + if (policyNoCol == -1 || commissionCol == -1) { + ToastHelper.showWarningToast( + context, + 'Excel is missing required columns: Policy Number and Commission Amount.', + ); + return; + } + + final Map uploadMap = {}; + final Map uploadPolicyDisplay = {}; + final Map uploadUtrMap = {}; + final Set seenUtrs = {}; + final Set duplicateUtrs = {}; + int invalidRows = 0; + + for (int rowIndex = 1; rowIndex < rows.length; rowIndex++) { + final row = rows[rowIndex]; + final policyRaw = + row.length > policyNoCol ? _excelCellValue(row[policyNoCol]) : ''; + final commissionRaw = + row.length > commissionCol ? _excelCellValue(row[commissionCol]) : ''; + final utrRaw = utrNoCol >= 0 && row.length > utrNoCol + ? _excelCellValue(row[utrNoCol]) + : ''; + + // Ignore fully empty Excel rows; do not count as invalid. + if (policyRaw.trim().isEmpty && + commissionRaw.trim().isEmpty && + utrRaw.trim().isEmpty) { + continue; + } + + final normalizedPolicy = _normalizePolicyNo(policyRaw); + final parsedCommission = _parseExcelCommission(commissionRaw); + + if (normalizedPolicy.isEmpty || parsedCommission == null) { + invalidRows++; + continue; + } + uploadMap[normalizedPolicy] = parsedCommission; + final display = policyRaw.trim().isEmpty ? normalizedPolicy : policyRaw.trim(); + uploadPolicyDisplay[normalizedPolicy] = display; + if (utrRaw.trim().isNotEmpty) { + final cleanedUtr = utrRaw.trim(); + // Enforce unique UTR numbers inside uploaded Excel file. + if (seenUtrs.contains(cleanedUtr)) { + duplicateUtrs.add(cleanedUtr); + } else { + seenUtrs.add(cleanedUtr); + uploadUtrMap[normalizedPolicy] = cleanedUtr; + } + } + } + + if (duplicateUtrs.isNotEmpty) { + ToastHelper.showWarningToast( + context, + 'UTR number repeated in Excel: ${duplicateUtrs.join(", ")}', + ); + return; + } + + if (uploadMap.isEmpty) { + ToastHelper.showWarningToast( + context, + 'No valid data rows in Excel (check policy numbers and commission amounts).', + ); + return; + } + + final Set screenPolicyKeys = { + for (final p in filteredPolicies) _normalizePolicyNo(p['policy_no']), + }; + + final List> matchedPolicyRefs = []; + final List> matchedRowsForDialog = []; + final List excelPoliciesNotOnTable = []; + int utrComparedCount = 0; + int utrChangedCount = 0; + int utrSameCount = 0; + + for (final MapEntry e in uploadMap.entries) { + final key = e.key; + if (screenPolicyKeys.contains(key)) { + final policy = filteredPolicies.firstWhere( + (p) => _normalizePolicyNo(p['policy_no']) == key, + ); + + final excelUtr = (uploadUtrMap[key] ?? '').trim(); + final tableUtr = _displayText(policy['utr_no']); + if (excelUtr.isNotEmpty) { + utrComparedCount++; + if (tableUtr != '-' && tableUtr == excelUtr) { + utrSameCount++; + } else { + utrChangedCount++; + } + } + + matchedPolicyRefs.add(policy); + matchedRowsForDialog.add({ + 'policy_no': (policy['policy_no'] ?? '').toString(), + 'current_commission': (policy['commission_amount'] ?? '').toString(), + 'excel_commission': uploadMap[key]!.toStringAsFixed(2), + 'given_utr_no': excelUtr, + }); + } else { + excelPoliciesNotOnTable.add(uploadPolicyDisplay[key] ?? key); + } + } + + final int matched = matchedRowsForDialog.length; + final int tableRowsNotInExcel = filteredPolicies + .where((p) => !uploadMap.containsKey(_normalizePolicyNo(p['policy_no']))) + .length; + + print( + '[PayOut Excel comparison] referAgents=${selectedAgentId?.length ?? 0} ' + 'brokerId=$brokerId | onScreenPolicies=${filteredPolicies.length} | ' + 'excelValidPolicies=${uploadMap.length} | matchedForUpdate=$matched | ' + 'excelNotOnTable=${excelPoliciesNotOnTable.length} | ' + 'tableRowsNotInExcel=$tableRowsNotInExcel | excelRowsSkippedInvalid=$invalidRows', + ); + + if (matched == 0) { + await _showAllPoliciesUnmatchedDialog( + excelPolicyNumbersNotMatched: excelPoliciesNotOnTable.isNotEmpty + ? excelPoliciesNotOnTable + : uploadMap.keys.map((k) => uploadPolicyDisplay[k] ?? k).toList(), + invalidExcelRows: invalidRows, + ); + return; + } + + final bool proceed = await _showUploadReviewProceedDialog( + matchedRows: matchedRowsForDialog, + excelPolicyNumbersNotOnTable: excelPoliciesNotOnTable, + invalidExcelRows: invalidRows, + ); + if (!proceed) { + ToastHelper.showWarningToast(context, 'Commission update cancelled'); + return; + } + + setState(() { + /* + * Excel upload action: + * - update commission values for matched policy rows. + * - capture optional UTR Number column per policy row. + * captured UTR values are sent when user clicks "Raise Invoice". + * - compare table utr_no vs excel UTR Number and refresh UI value. + */ + for (final policy in matchedPolicyRefs) { + final key = _normalizePolicyNo(policy['policy_no']); + if (uploadMap.containsKey(key)) { + policy['commission_amount'] = uploadMap[key]!.toStringAsFixed(2); + } + if (uploadUtrMap.containsKey(key)) { + _uploadedUtrByPolicyKey[key] = uploadUtrMap[key]!; + policy['utr_no'] = uploadUtrMap[key]!; + } + } + }); + + _calculateTotalCommission(); + + final message = + 'Updated $matched policy commission(s) from Excel' + '${utrComparedCount > 0 ? ' | UTR compared: $utrComparedCount (same: $utrSameCount, changed: $utrChangedCount)' : ''}' + '${excelPoliciesNotOnTable.isNotEmpty ? ' | Excel-only (ignored): ${excelPoliciesNotOnTable.length}' : ''}' + '${tableRowsNotInExcel > 0 ? ' | table rows not in Excel: $tableRowsNotInExcel' : ''}' + '${invalidRows > 0 ? ' | invalid Excel rows: $invalidRows' : ''}'; + ToastHelper.showSuccessToast(context, message); + } catch (e) { + ToastHelper.showErrorToast(context, e.toString()); + } finally { + if (mounted) { + setState(() => isLoading = false); + } + } + } + void showMessage(String msg) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } @@ -718,7 +1291,7 @@ class _PayOutScreenState extends ConsumerState { color: Color(0xFF425B5B), ), onPressed: () { - context.go(AppRoutes.invoiceList); + context.go(AppRoutes.payoutList); }, splashRadius: 18, hoverColor: Colors.black12, @@ -792,92 +1365,112 @@ class _PayOutScreenState extends ConsumerState { if (!isEdit) ...[ const Spacer(), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, + OutlinedButton.icon( + onPressed: isLoading ? null : _triggerBulkUpload, + icon: const Icon(Icons.upload_file, size: 16), + label: Text( + "Upload", + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.w500, + ), ), - decoration: BoxDecoration( - color: const Color(0xFFECFDF5), // emerald bg - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - Text( - "SELECTED POLICIES : ", - style: GoogleFonts.poppins( - fontSize: 10, - fontWeight: FontWeight.w500, - color: const Color(0xFF065F46), - letterSpacing: 0.4, - ), - ), - const SizedBox(width: 6), - Text( - // "15", - // selectedPolicies.length.toString(), - totalPolicies, - style: GoogleFonts.poppins( - fontSize: 13, - fontWeight: FontWeight.w700, - color: const Color(0xFF047857), - ), - ), - ], + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Color(0xFF2E7D6E)), + foregroundColor: const Color(0xFF2E7D6E), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), ), ), - - const SizedBox(width: 10), - - // 💰 TOTAL COMMISSION - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: const Color(0xFF10B981), // emerald green - borderRadius: BorderRadius.circular(22), - boxShadow: [ - BoxShadow( - color: const Color( - 0xFF10B981, - ).withOpacity(0.25), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - const Icon( - Icons.account_balance_wallet_outlined, - size: 14, - color: Colors.white, - ), - const SizedBox(width: 6), - Text( - "TOTAL COMMISSION :", - style: GoogleFonts.poppins( - fontSize: 10, - fontWeight: FontWeight.w500, - color: Colors.white.withOpacity(0.9), - letterSpacing: 0.4, + if (hasFetchedTableData) ...[ + const SizedBox(width: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: const Color(0xFFECFDF5), // emerald bg + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Text( + "SELECTED POLICIES : ", + style: GoogleFonts.poppins( + fontSize: 10, + fontWeight: FontWeight.w500, + color: const Color(0xFF065F46), + letterSpacing: 0.4, + ), ), - ), - const SizedBox(width: 6), - Text( - // "₹${_calculateTotalCommission()}", - "₹$totalCommission", - style: GoogleFonts.poppins( - fontSize: 14, - fontWeight: FontWeight.w700, + const SizedBox(width: 6), + Text( + totalPolicies, + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w700, + color: const Color(0xFF047857), + ), + ), + ], + ), + ), + + const SizedBox(width: 10), + + // 💰 TOTAL COMMISSION + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: const Color(0xFF10B981), // emerald green + borderRadius: BorderRadius.circular(22), + boxShadow: [ + BoxShadow( + color: const Color( + 0xFF10B981, + ).withOpacity(0.25), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + const Icon( + Icons.account_balance_wallet_outlined, + size: 14, color: Colors.white, ), - ), - ], + const SizedBox(width: 6), + Text( + "TOTAL COMMISSION :", + style: GoogleFonts.poppins( + fontSize: 10, + fontWeight: FontWeight.w500, + color: Colors.white.withOpacity(0.9), + letterSpacing: 0.4, + ), + ), + const SizedBox(width: 6), + Text( + "₹$totalCommission", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ], + ), ), - ), + const SizedBox(width: 10), + ], ], ], ), @@ -947,14 +1540,17 @@ class _PayOutScreenState extends ConsumerState { ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: const SizedBox(), - secondChild: Column( - children: [ - _policyTableHeader(), // 🔒 Static header - Container( - // color: Colors.amber.shade100, - child: _policyTableBody(), - ), // 🔽 Scrollable rows - ], + secondChild: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: _tableMinWidth, + child: Column( + children: [ + _policyTableHeader(), // static header + _policyTableBody(), // vertical scroll rows + ], + ), + ), ), ), ], @@ -1214,6 +1810,7 @@ class _PayOutScreenState extends ConsumerState { _headerText("REFERER", colAgent), _headerText("CUSTOMER", colCustomer), _headerText("PREMIUM", colPremium), + _headerText("UTR NO", colUtr), _headerText("COMMISSION", colCommission), ], ), @@ -1301,15 +1898,51 @@ class _PayOutScreenState extends ConsumerState { ), _cell(p["customer_name"], colCustomer), _cell("₹${p["premium_amount"]}", colPremium), + _cell(_displayText(p["utr_no"]), colUtr), SizedBox( width: colCommission, - child: Text( - "₹${p["commission_amount"]}", - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Color(0xFF009B77), // Emerald green - ), + child: Row( + children: [ + const Text( + "₹", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF009B77), + ), + ), + const SizedBox(width: 2), + Expanded( + child: TextFormField( + key: ValueKey( + 'commission_${p["policy_id"]}_${p["commission_amount"]}', + ), + initialValue: (p["commission_amount"] ?? '') + .toString(), + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF009B77), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + inputFormatters: _commissionInputFormatter, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 6, + vertical: 6, + ), + border: OutlineInputBorder(), + ), + onChanged: (value) { + p["commission_amount"] = value.trim(); + _calculateTotalCommission(); + }, + ), + ), + ], ), ), ], @@ -1321,6 +1954,11 @@ class _PayOutScreenState extends ConsumerState { ); } + double _parseCommissionValue(dynamic value) { + if (value == null) return 0; + return double.tryParse(value.toString().trim()) ?? 0; + } + Widget _cell(String? value, double width) { return SizedBox( width: width, @@ -1344,11 +1982,7 @@ class _PayOutScreenState extends ConsumerState { if (!selectedPolicies.contains(id)) continue; - // Safe commission amount - final String commStr = p["commission_amount"]?.toString() ?? "0"; - final double comm = double.tryParse(commStr) ?? 0; - - total += comm; + total += _parseCommissionValue(p["commission_amount"]); } setState(() { totalCommission = total.roundToDouble(); @@ -1552,7 +2186,18 @@ class _PayOutScreenState extends ConsumerState { static final double colAgent = 200; static final double colCustomer = 200; static final double colPremium = 200; + static final double colUtr = 170; // static final double colCommission = 200; double get colCommission => isEdit ? 120 : 200; + double get _tableMinWidth => + (isEdit ? 0 : colCheck) + + colPolicy + + colDate + + colAgent + + colCustomer + + colPremium + + colUtr + + colCommission + + 32; } diff --git a/lib/presentation/screens/staff/policy/policy_list.dart b/lib/presentation/screens/staff/policy/policy_list.dart index c2b9353..39fa525 100644 --- a/lib/presentation/screens/staff/policy/policy_list.dart +++ b/lib/presentation/screens/staff/policy/policy_list.dart @@ -38,6 +38,12 @@ class policylistState extends ConsumerState { dynamic SelectedStatus; dynamic SelectedInsurer; dynamic SelectedStaffId; + /// Partner (agent) id for Accounts policy list filter. + String? SelectedPartnerId; + /// Partner `agent_code` for payout sample Excel when Accounts. + String selectedPartnerAgentCode = ''; + /// From staff dropdown (manager/handler) for payout sample when set. + String selectedStaffAgentCode = ''; // List> dataVal = []; List> getStaffData = []; List> originalData = []; @@ -146,8 +152,10 @@ class policylistState extends ConsumerState { fromDate: fromDt, toDate: toDt, selectedStaffId: SelectedStaffId ?? '', - selectedStatus:selectedStatus, - selectedInsurer:selectedInsurer + selectedStatus: selectedStatus, + selectedInsurer: selectedInsurer, + selectedAgentId: + role == 'Accounts' ? (SelectedPartnerId ?? '') : null, ); if (response['status'] == 'success') { @@ -245,6 +253,9 @@ class policylistState extends ConsumerState { void refrshfilterDateRange() { setState(() { SelectedStaffId = null; + SelectedPartnerId = null; + selectedPartnerAgentCode = ''; + selectedStaffAgentCode = ''; controllers['startDate']!.clear(); controllers['endDate']!.clear(); SelectedStatus = (roleId == 'Accounts') ? 'Not Verified' : 'All'; // ✅ @@ -341,6 +352,35 @@ class policylistState extends ConsumerState { // } } + Future downloadPendingCommissionPayoutSample() async { + final fromText = controllers['startDate']?.text.trim() ?? ''; + final toText = controllers['endDate']?.text.trim() ?? ''; + if (fromText.isEmpty || toText.isEmpty) { + ToastHelper.showErrorToast(context, 'Please select start and end dates'); + return; + } + try { + final fromDt = DateFormat('dd-MM-yyyy').parse(fromText); + final toDt = DateFormat('dd-MM-yyyy').parse(toText); + final fromIso = DateFormat('yyyy-MM-dd').format(fromDt); + final toIso = DateFormat('yyyy-MM-dd').format(toDt); + await apiService.downloadPendingCommissionExcel( + fromDate: fromIso, + toDate: toIso, + agentCode: selectedPartnerAgentCode.isNotEmpty + ? selectedPartnerAgentCode + : selectedStaffAgentCode, + ); + } catch (e) { + if (mounted) { + ToastHelper.showErrorToast( + context, + 'Could not download file. Check dates or try again.', + ); + } + } + } + @override Widget build(BuildContext context) { return MainLayout( @@ -440,6 +480,22 @@ class policylistState extends ConsumerState { print('Selected Filterd STAFF Id - $val'); SelectedStaffId = val; }, + onStaffAgentCodeChanged: (code) { + setState(() { + selectedStaffAgentCode = code ?? ''; + }); + }, + selectedPartnerId: SelectedPartnerId, + onPartnerChanged: (id) { + setState(() { + SelectedPartnerId = id; + }); + }, + onPartnerAgentCodeChanged: (code) { + setState(() { + selectedPartnerAgentCode = code ?? ''; + }); + }, selectedStaffId: SelectedStaffId, startController: controllers['startDate']!, endController: controllers['endDate']!, @@ -485,6 +541,22 @@ class policylistState extends ConsumerState { print('Selected Filterd STAFF Id - $val'); SelectedStaffId = val; }, + onStaffAgentCodeChanged: (code) { + setState(() { + selectedStaffAgentCode = code ?? ''; + }); + }, + selectedPartnerId: SelectedPartnerId, + onPartnerChanged: (id) { + setState(() { + SelectedPartnerId = id; + }); + }, + onPartnerAgentCodeChanged: (code) { + setState(() { + selectedPartnerAgentCode = code ?? ''; + }); + }, selectedStaffId: SelectedStaffId, startController: controllers['startDate']!, endController: controllers['endDate']!, @@ -589,6 +661,26 @@ class policylistState extends ConsumerState { ), ), ), + SizedBox(width: 8), + Tooltip( + message: + 'Download payout sample Excel (fill Payout Amount and re-upload)', + child: InkWell( + onTap: () => downloadPendingCommissionPayoutSample(), + child: Container( + padding: const EdgeInsets.all(7.0), + decoration: BoxDecoration( + color: const Color(0xFF1565C0), + borderRadius: BorderRadius.circular(8.0), + ), + child: Icon( + Icons.description_outlined, + size: 15, + color: Colors.white, + ), + ), + ), + ), SizedBox(width: 10), ], ), diff --git a/lib/presentation/screens/staff/policy/policy_validation.dart b/lib/presentation/screens/staff/policy/policy_validation.dart index 803f8c8..b94d961 100644 --- a/lib/presentation/screens/staff/policy/policy_validation.dart +++ b/lib/presentation/screens/staff/policy/policy_validation.dart @@ -45,6 +45,7 @@ class _policyValidationState extends ConsumerState { bool isLoadingBroker = false; bool isLoadingPaymentMode = false; bool isLoadingInsurancePlan = false; + bool isCommissionManuallyEditable = false; dynamic managerId; dynamic userId; @@ -1425,6 +1426,62 @@ class _policyValidationState extends ConsumerState { print('_fetchCommision OUT'); } + bool get _showChangeCommissionAction { + final String status = + (widget.item?['is_data_accuracy_checked']?.toString() ?? '').trim(); + return status == '0' || status == '1'; + } + + bool get _isDataAccuracyChecked { + final String status = + (widget.item?['is_data_accuracy_checked']?.toString() ?? '').trim(); + return status == '1'; + } + + Future _updateCommissionOnly() async { + final dynamic policyId = widget.item?['policy_id'] ?? widget.item?['id']; + final String commissionText = + controllers['commission_amount']?.text.trim() ?? ''; + + if (policyId == null) { + ToastHelper.showWarningToast(context, 'Missing policy id'); + return; + } + + final double? commission = double.tryParse(commissionText); + if (commission == null) { + ToastHelper.showWarningToast(context, 'Please enter valid commission amount'); + return; + } + + setState(() => isLoading = true); + try { + final payload = { + 'id': policyId, + 'commission_amount': commission.toStringAsFixed(2), + 'updated_by': userId, + }; + + final result = await apiService.updatePolicyCommissionApi(payload); + if (result['status'] == 'success') { + if (!mounted) return; + setState(() => isCommissionManuallyEditable = false); + ToastHelper.showSuccessToast(context, 'Commission updated successfully'); + } else { + ToastHelper.showErrorToast( + context, + result['data']?.toString() ?? + result['message']?.toString() ?? + 'Failed to update commission', + ); + } + } catch (e) { + ToastHelper.showErrorToast(context, 'Exception: $e'); + } finally { + if (mounted) setState(() => isLoading = false); + } + } + Widget buildVehicleType(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -2939,6 +2996,8 @@ class _policyValidationState extends ConsumerState { 'Commission Amount *', controllers['commission_amount']!, required: false, + readOnly: + !isCommissionManuallyEditable, keyboardType: TextInputType.numberWithOptions( decimal: true, @@ -2953,10 +3012,39 @@ class _policyValidationState extends ConsumerState { mainAxisAlignment: MainAxisAlignment .end, // Pushes children to the right children: [ + if (_showChangeCommissionAction) + TextButton( + onPressed: isLoading + ? null + : () async { + /* + * First click: allow manual edit. + * Next click while editable: submit to updatePolicyCommission API. + */ + if (!isCommissionManuallyEditable) { + setState(() { + isCommissionManuallyEditable = + true; + }); + return; + } + await _updateCommissionOnly(); + }, + child: Text( + isCommissionManuallyEditable + ? 'Update Commission' + : 'Change Commission', + style: TextStyle( + color: isLoading + ? Colors.grey + : const Color(0xFF2E7D6E), + fontWeight: FontWeight.bold, + ), + ), + ), TextButton( onPressed: - (widget.item?['is_data_accuracy_checked'] == - '1' || + (_isDataAccuracyChecked || isLoading) ? null // Disable if already checked or currently loading : _fetchCommision, @@ -2966,8 +3054,7 @@ class _policyValidationState extends ConsumerState { : "Calculate", style: TextStyle( color: - (widget.item?['is_data_accuracy_checked'] == - '1' || + (_isDataAccuracyChecked || isLoading) ? Colors.grey : const Color( diff --git a/lib/presentation/themes/indicators/customizd_file_upload.dart b/lib/presentation/themes/indicators/customizd_file_upload.dart index 1837c01..fc8aa29 100644 --- a/lib/presentation/themes/indicators/customizd_file_upload.dart +++ b/lib/presentation/themes/indicators/customizd_file_upload.dart @@ -14,12 +14,15 @@ class ThemedUploadField extends StatefulWidget { final Color? borderColor; final Color? errorBorderColor; final Function(String fileName, PlatformFile file)? onFileSelected; + final Function(List fileNames, List files)? + onFilesSelected; FormFieldValidator? validator; final List? allowedExtensions; final bool isTxtBtnCase; final String? txtName; final double? padVertical; final double? padHorizontal; + final bool allowMultiple; ThemedUploadField({ super.key, @@ -32,12 +35,14 @@ class ThemedUploadField extends StatefulWidget { this.borderColor, this.errorBorderColor, this.onFileSelected, + this.onFilesSelected, this.validator, this.allowedExtensions, this.isTxtBtnCase = false, this.txtName, this.padVertical, this.padHorizontal, + this.allowMultiple = false, }); @override @@ -47,6 +52,7 @@ class ThemedUploadField extends StatefulWidget { class _ThemedUploadFieldState extends State { final fileService = FileUploadService(); String? selectedFileName; + String? selectedMultipleFileNames; String? errorMessage; // @override @@ -62,16 +68,27 @@ class _ThemedUploadFieldState extends State { // } Future _pickFile() async { - final error = await fileService.pickSingleFile( - // maxFileSizeInMB: 3, - allowedExtensions: widget.allowedExtensions, - ); + final error = widget.allowMultiple + ? await fileService.pickMultipleFiles( + allowedExtensions: widget.allowedExtensions, + ) + : await fileService.pickSingleFile( + allowedExtensions: widget.allowedExtensions, + ); if (error != null) { setState(() => errorMessage = error); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(error))); - } else if (fileService.singleFile != null) { + } else if (widget.allowMultiple && fileService.multipleFiles.isNotEmpty) { + final names = fileService.multipleFiles.map((file) => file.name).toList(); + setState(() { + selectedMultipleFileNames = names.join(', '); + errorMessage = null; + }); + + widget.onFilesSelected?.call(names, fileService.multipleFiles); + } else if (!widget.allowMultiple && fileService.singleFile != null) { setState(() { selectedFileName = fileService.singleFile!.name; errorMessage = null; @@ -122,7 +139,9 @@ class _ThemedUploadFieldState extends State { if (!widget.isTxtBtnCase) ...[ Expanded( child: Text( - selectedFileName ?? + (widget.allowMultiple + ? selectedMultipleFileNames + : selectedFileName) ?? widget.hintText ?? "Upload Document", style: TextStyle( diff --git a/lib/presentation/themes/indicators/date_field_theme.dart b/lib/presentation/themes/indicators/date_field_theme.dart index 6687ccf..b75cb9c 100644 --- a/lib/presentation/themes/indicators/date_field_theme.dart +++ b/lib/presentation/themes/indicators/date_field_theme.dart @@ -16,6 +16,7 @@ class ThemedDateField extends StatefulWidget { this.controller, this.validator, this.lastDate, // ✅ new + this.firstDate, }); final String? hintText; @@ -28,6 +29,7 @@ class ThemedDateField extends StatefulWidget { final Function(DateTime)? onDateSelected; final String? Function(String? value)? validator; // ✅ new final DateTime? lastDate; + final DateTime? firstDate; @override State createState() => _ThemedDateFieldState(); @@ -48,7 +50,7 @@ class _ThemedDateFieldState extends State { final pickedDate = await showDatePicker( context: context, initialDate: initialDate, - firstDate: DateTime(2000), + firstDate: widget.firstDate ?? DateTime(2000), // lastDate: DateTime(2100), lastDate: widget.lastDate ?? DateTime(2100), initialEntryMode: DatePickerEntryMode.calendarOnly, diff --git a/lib/presentation/themes/indicators/export_btn.dart b/lib/presentation/themes/indicators/export_btn.dart index 9f81926..c598413 100644 --- a/lib/presentation/themes/indicators/export_btn.dart +++ b/lib/presentation/themes/indicators/export_btn.dart @@ -18,6 +18,13 @@ import 'dart:io' show File; // Only used in mobile/desktop import 'package:path_provider/path_provider.dart'; import 'package:open_file/open_file.dart'; +/// Optional per-field Excel cell formatter. Use when raw map values differ from +/// on-screen display (dates, status labels, etc.). Key must match [keys] entries. +typedef ExportValueFormatter = String Function( + dynamic value, + Map row, +); + class ExportBtn extends HookWidget { final String sheetName; final String fileName; @@ -26,6 +33,8 @@ class ExportBtn extends HookWidget { // final List headers; // dynamic headers final List? keys; // dynamic headers final bool? txt; + /// When set, these keys are exported as the returned string (not raw int/double). + final Map? valueFormatters; const ExportBtn({ super.key, @@ -37,6 +46,7 @@ class ExportBtn extends HookWidget { // required this.headers, required this.displayHeaders, this.keys, + this.valueFormatters, }); @override @@ -54,6 +64,7 @@ class ExportBtn extends HookWidget { displayHeaders: displayHeaders!, keys: keys!, fileName: fileName, + valueFormatters: valueFormatters, ); ToastHelper.showSuccessToast(context, 'Export finished'); } catch (e) { @@ -117,6 +128,7 @@ class ExcelExporter { required List displayHeaders, required List keys, required String fileName, + Map? valueFormatters, }) async { if (displayHeaders.length != keys.length) { throw ArgumentError('displayHeaders and keys must have same length'); @@ -169,6 +181,7 @@ class ExcelExporter { // ✅ Reverse the data list so latest entries come first final reversedData = data.reversed.toList(); + final formatters = valueFormatters; for (var i = 0; i < reversedData.length; i++) { final rowMap = reversedData[i]; @@ -180,6 +193,11 @@ class ExcelExporter { if (key == 'sno' || key.toLowerCase() == 's.no' || key.toLowerCase() == 'sno.') { value = i + 1; + } else if (formatters != null && formatters.containsKey(key)) { + final formatted = formatters[key]!(rowMap[key], rowMap); + final text = formatted.trim().isEmpty ? '-' : formatted; + row.add(TextCellValue(text)); + continue; } else if (key.toLowerCase() == 'is_active') { final rawVal = rowMap[key]; value = (rawVal == 1 || rawVal == '1') ? 'Active' : 'Inactive'; diff --git a/lib/presentation/themes/indicators/fileUploadService.dart b/lib/presentation/themes/indicators/fileUploadService.dart index a06c485..500a786 100644 --- a/lib/presentation/themes/indicators/fileUploadService.dart +++ b/lib/presentation/themes/indicators/fileUploadService.dart @@ -6,6 +6,7 @@ class FileUploadService { factory FileUploadService() => _instance; PlatformFile? singleFile; + List multipleFiles = []; /// Default allowed extensions static const defaultAllowedExtensions = ['pdf', 'png', 'jpg', 'jpeg']; @@ -40,7 +41,35 @@ class FileUploadService { return null; // success } + Future pickMultipleFiles({ + int maxFileSizeInMB = 5, + List? allowedExtensions, + }) async { + final extensions = allowedExtensions ?? defaultAllowedExtensions; + final result = await FilePicker.platform.pickFiles( + allowMultiple: true, + withData: true, + type: FileType.custom, + allowedExtensions: extensions, + ); + + if (result != null && result.files.isNotEmpty) { + for (final file in result.files) { + final ext = file.extension?.toLowerCase() ?? ''; + if (!extensions.contains(ext)) { + return "Unsupported format: ${file.name}"; + } + } + multipleFiles = result.files; + } + return null; + } + void clearSingle() { singleFile = null; } + + void clearMultiple() { + multipleFiles = []; + } } diff --git a/lib/presentation/widgets/custom_Stdate_EnDate_Filter.dart b/lib/presentation/widgets/custom_Stdate_EnDate_Filter.dart index 9f76027..ddc8f41 100644 --- a/lib/presentation/widgets/custom_Stdate_EnDate_Filter.dart +++ b/lib/presentation/widgets/custom_Stdate_EnDate_Filter.dart @@ -43,6 +43,12 @@ class DateFilterRow extends ConsumerStatefulWidget { final ValueChanged? onStatusChanged; final ValueChanged? onInsurerChanged; final ValueChanged? onFilterStaff; + /// Staff row may include `agent_code` (e.g. payout sample Excel for manager/handler). + final ValueChanged? onStaffAgentCodeChanged; + /// Accounts + Policy: selected partner `id` from [agentListForEnquiryCreationDropdown]. + final String? selectedPartnerId; + final ValueChanged? onPartnerChanged; + final ValueChanged? onPartnerAgentCodeChanged; final String? dataFrom; final VoidCallback onFilter; final VoidCallback onRefresh; @@ -60,6 +66,10 @@ class DateFilterRow extends ConsumerStatefulWidget { required this.onStatusChanged, this.onInsurerChanged, this.onFilterStaff, + this.onStaffAgentCodeChanged, + this.selectedPartnerId, + this.onPartnerChanged, + this.onPartnerAgentCodeChanged, required this.selectedStaffId, this.selectedStatusVal, this.selectedInsurerVal, @@ -83,6 +93,7 @@ class _DateFilterRowState extends ConsumerState { Map? selectedInsurerData; List> filteredInsurerData = []; List> getInsurerDetailsData = []; + List> filteredPartnerData = []; String? selectedInsurer; String? selectedStaff; @@ -92,6 +103,7 @@ class _DateFilterRowState extends ConsumerState { bool isLoadingA = false; bool isLoading = false; + bool _partnerFetchAttempted = false; @override void initState() { @@ -110,6 +122,11 @@ class _DateFilterRowState extends ConsumerState { } print('***Insurer***'); getInsurerDetails(); + if (widget.role == 'Accounts' && + widget.dataFrom == 'Policy' && + managerId != null) { + getPartnerListForAccountsPolicy(); + } }); // Future.microtask(() { @@ -190,8 +207,45 @@ class _DateFilterRowState extends ConsumerState { } } + Future getPartnerListForAccountsPolicy() async { + if (widget.role != 'Accounts' || widget.dataFrom != 'Policy') return; + if (_partnerFetchAttempted) return; + _partnerFetchAttempted = true; + + final mid = managerId ?? ref.read(managerIdProvider); + if (mid == null) return; + final managerIdInt = mid is int ? mid : int.tryParse(mid.toString()); + if (managerIdInt == null) return; + try { + final response = await apiService.fetchAgentUserList(managerIdInt); + if (response['status'] == 'success' && response['data'] != null) { + setState(() { + filteredPartnerData = List>.from( + response['data'], + ); + }); + } else { + setState(() => filteredPartnerData = []); + } + } catch (e) { + print('getPartnerListForAccountsPolicy: $e'); + setState(() => filteredPartnerData = []); + } + } + @override Widget build(BuildContext context) { + if (widget.role == 'Accounts' && + widget.dataFrom == 'Policy' && + filteredPartnerData.isEmpty && + !_partnerFetchAttempted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + getPartnerListForAccountsPolicy(); + } + }); + } + final spacing = 5.0; // final rowChildren = [ @@ -230,7 +284,9 @@ class _DateFilterRowState extends ConsumerState { buildSelectStaffMem(context), SizedBox(width: spacing), ], - if (widget.role == 'Accounts' && widget.dataFrom == 'Policy') ...[ + if (widget.role == 'Accounts' && widget.dataFrom == 'Policy') ...[ + buildSelectPartnerAccountsPolicy(context), + SizedBox(width: spacing), buildPolicyReportFlagSearch(context), SizedBox(width: spacing), buildSelectInsurer(context), @@ -669,6 +725,148 @@ class _DateFilterRowState extends ConsumerState { ); } + /// Searchable partner (agent) list for **Accounts** on **Policy** — `agent/agentList` via [ApiService.fetchAgentUserList]. + Widget buildSelectPartnerAccountsPolicy(BuildContext context) { + if (widget.role != 'Accounts' || widget.dataFrom != 'Policy') { + return const SizedBox.shrink(); + } + + Map? selectedPartner; + final pid = widget.selectedPartnerId; + if (pid != null && + pid.toString().isNotEmpty && + filteredPartnerData.isNotEmpty) { + try { + selectedPartner = filteredPartnerData.firstWhere( + (e) => e['id'].toString() == pid.toString(), + ); + } catch (_) { + selectedPartner = null; + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 35, + child: Container( + width: ResponsiveLayout.isMobile(context) + ? null + : MediaQuery.of(context).size.width * 0.14, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.0), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: DropdownSearch>( + selectedItem: selectedPartner, + items: (filter, infiniteScrollProps) async { + if (filter.isEmpty) return filteredPartnerData; + final q = filter.toLowerCase(); + return filteredPartnerData.where((item) { + final name = item['name']?.toString().toLowerCase() ?? ''; + final code = + item['agent_code']?.toString().toLowerCase() ?? ''; + return name.contains(q) || code.contains(q); + }).toList(); + }, + itemAsString: (val) => val['name'].toString(), + compareFn: (item, selectedItem) => + item['id'].toString() == selectedItem['id'].toString(), + 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, + ), + ), + decoratorProps: DropDownDecoratorProps( + decoration: + AppInputDecorations.dropdownDecoration( + label: 'Partner Name', + ).copyWith( + filled: true, + fillColor: Colors.white, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 1, + ), + ), + ), + popupProps: PopupProps.menu( + fit: FlexFit.loose, + constraints: const BoxConstraints(maxHeight: 280), + menuProps: const MenuProps(backgroundColor: Colors.white), + showSearchBox: true, + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + filled: true, + fillColor: Colors.white, + hintText: 'Partner Name', + hintStyle: GoogleFonts.inter( + fontSize: 10, + color: Colors.grey, + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.white, width: 1.5), + ), + ), + ), + itemBuilder: (context, item, isDisabled, isSelected) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 6, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (item['name'] ?? '').toString().toUpperCase(), + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + Text( + (item['agent_code'] ?? '').toString().toUpperCase(), + style: GoogleFonts.inter( + fontSize: 11, + color: Colors.grey, + ), + ), + ], + ), + ); + }, + ), + onChanged: (val) { + if (val != null) { + widget.onPartnerChanged?.call(val['id']?.toString()); + widget.onPartnerAgentCodeChanged?.call( + val['agent_code']?.toString(), + ); + Future.microtask(() => widget.onFilter()); + } else { + widget.onPartnerChanged?.call(null); + widget.onPartnerAgentCodeChanged?.call(null); + Future.microtask(() => widget.onFilter()); + } + }, + ), + ), + ), + ], + ); + } + Widget buildSelectStaffMem(BuildContext context) { // Map? selectedVehicle = filteredStaffData.firstWhere( // (item) => item['id'].toString() == selectedStaff, @@ -819,9 +1017,15 @@ class _DateFilterRowState extends ConsumerState { if (widget.onFilterStaff != null) widget.onFilterStaff!(val['id']); + widget.onStaffAgentCodeChanged?.call( + val['agent_code']?.toString(), + ); + Future.microtask(() => widget.onFilter()); // controllers['agentId']?.text = val['agent_code']; // agentId = agent['id']; + } else { + widget.onStaffAgentCodeChanged?.call(null); } }, ), diff --git a/lib/presentation/widgets/drawerContentWrapper.dart b/lib/presentation/widgets/drawerContentWrapper.dart index 9bf7891..a424afd 100644 --- a/lib/presentation/widgets/drawerContentWrapper.dart +++ b/lib/presentation/widgets/drawerContentWrapper.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; class DrawerContentWrapper extends StatelessWidget { final Widget child; + final bool isActive; - const DrawerContentWrapper({super.key, required this.child}); + const DrawerContentWrapper({super.key, required this.child,this.isActive = false}); @override Widget build(BuildContext context) { @@ -15,7 +16,7 @@ class DrawerContentWrapper extends StatelessWidget { width: 55, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0), - color: const Color(0xFFB2D8D3), + color: isActive ? const Color(0xFF0ABFA3) : const Color(0xFFB2D8D3), ), child: Center(child: child), ), diff --git a/lib/presentation/widgets/drawer_menu.dart b/lib/presentation/widgets/drawer_menu.dart index 34acbad..b7f6b07 100644 --- a/lib/presentation/widgets/drawer_menu.dart +++ b/lib/presentation/widgets/drawer_menu.dart @@ -17,7 +17,13 @@ class DrawerMenu extends ConsumerStatefulWidget { class DrawerMenuState extends ConsumerState { OverlayEntry? _overlayEntry; - void _showPopup(BuildContext context, Offset offset, Size size, String key) { + void _showPopup( + BuildContext context, + Offset offset, + Size size, + String key, + dynamic roleId, + ) { _hidePopup(); // remove previous popup _overlayEntry = OverlayEntry( @@ -52,6 +58,19 @@ class DrawerMenuState extends ConsumerState { ), ], if (key == 'Reports') ...[ + if (roleId == 'manager' || + roleId == 'Accounts' || + roleId == 'agent') ...[ + _buildPopupItem( + label: "Grid List", + onTap: () => context.go( + roleId == 'agent' + ? AppRoutes.gridView + : AppRoutes.gridList, + ), + ), + const SizedBox(height: 5), + ], _buildPopupItem( label: "Claims", onTap: () => context.go(AppRoutes.claimlist), @@ -94,6 +113,7 @@ class DrawerMenuState extends ConsumerState { @override Widget build(BuildContext context) { final roleId = ref.watch(userRoleProvider); + final currentPath = GoRouterState.of(context).matchedLocation; print('RoleId - $roleId'); return Container( @@ -113,6 +133,9 @@ class DrawerMenuState extends ConsumerState { width: 25, ), label: "Dashboard", + isActive: + currentPath == AppRoutes.dashboard || + currentPath == AppRoutes.partnerPortalDashboard, onTap: () async { _hidePopup(); @@ -120,7 +143,10 @@ class DrawerMenuState extends ConsumerState { await prefs.remove('dashboardKeyProvider'); await prefs.remove('dashboardStatusProvider'); await prefs.remove('dashboardStaffIdProvider'); - context.go(AppRoutes.dashboard); + final dashboardRoute = roleId == 'agent' + ? AppRoutes.partnerPortalDashboard + : AppRoutes.dashboard; + context.go(dashboardRoute); }, ), @@ -169,6 +195,7 @@ class DrawerMenuState extends ConsumerState { popupKey: 'Reports', ), + if (roleId == 'manager') _buildMenuItem( context: context, @@ -187,6 +214,7 @@ class DrawerMenuState extends ConsumerState { required BuildContext context, required Widget icon, required String label, + bool isActive = false, required VoidCallback onTap, }) { return Column( @@ -195,10 +223,10 @@ class DrawerMenuState extends ConsumerState { onTap: onTap, child: MouseRegion( onEnter: (_) => _hidePopup(), // hide any popup when hovering - child: DrawerContentWrapper(child: icon), + child: DrawerContentWrapper(child: icon, isActive: isActive), ), ), - DrawerLabel(label), + DrawerLabel(label, isActive: isActive), ], ); } @@ -218,7 +246,8 @@ class DrawerMenuState extends ConsumerState { final renderBox = itemContext.findRenderObject() as RenderBox; final offset = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; - _showPopup(context, offset, size, popupKey); + final roleId = ref.read(userRoleProvider); + _showPopup(context, offset, size, popupKey, roleId); }, child: DrawerContentWrapper(child: icon), ); @@ -233,7 +262,8 @@ class DrawerMenuState extends ConsumerState { // Drawer label widget class DrawerLabel extends StatelessWidget { final String text; - const DrawerLabel(this.text, {super.key}); + final bool isActive; + const DrawerLabel(this.text, {super.key, this.isActive = false}); @override Widget build(BuildContext context) { @@ -245,7 +275,11 @@ class DrawerLabel extends StatelessWidget { children: parts.map((line) { return Text( line, - style: GoogleFonts.inter(color: Colors.white, fontSize: 11.5), + style: GoogleFonts.inter( + color: isActive ? const Color(0xFF0ABFA3) : Colors.white, + fontSize: 11.5, + fontWeight: isActive ? FontWeight.w700 : FontWeight.w400, + ), textAlign: TextAlign.center, ); }).toList(), diff --git a/lib/presentation/widgets/drawer_menu1.dart b/lib/presentation/widgets/drawer_menu1.dart index 16f1067..8455576 100644 --- a/lib/presentation/widgets/drawer_menu1.dart +++ b/lib/presentation/widgets/drawer_menu1.dart @@ -17,7 +17,13 @@ class DrawerMenu extends ConsumerStatefulWidget { class DrawerMenuState extends ConsumerState { OverlayEntry? _overlayEntry; - void _showPopup(BuildContext context, Offset offset, Size size, String key) { + void _showPopup( + BuildContext context, + Offset offset, + Size size, + String key, + dynamic roleId, + ) { _hidePopup(); // clear old one before creating new _overlayEntry = OverlayEntry( @@ -56,6 +62,19 @@ class DrawerMenuState extends ConsumerState { ), ], if (key == 'Reports') ...[ + if (roleId == 'manager' || + roleId == 'Accounts' || + roleId == 'agent') ...[ + _buildPopupItem( + label: "Grid List", + onTap: () => context.go( + roleId == 'agent' + ? AppRoutes.gridView + : AppRoutes.gridList, + ), + ), + const SizedBox(height: 5), + ], _buildPopupItem( label: "Claims", onTap: () => context.go(AppRoutes.claimlist), @@ -256,7 +275,7 @@ class DrawerMenuState extends ConsumerState { final offset = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; final key = 'User'; - _showPopup(context, offset, size, key); + _showPopup(context, offset, size, key, roleId); }, child: DrawerContentWrapper( @@ -279,7 +298,7 @@ class DrawerMenuState extends ConsumerState { final offset = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; final key = "Reports"; - _showPopup(context, offset, size, key); + _showPopup(context, offset, size, key, roleId); }, child: DrawerContentWrapper( @@ -294,6 +313,7 @@ class DrawerMenuState extends ConsumerState { ), DrawerLabel("Reports"), ], + ], ), ); diff --git a/lib/presentation/widgets/mobile_tabs.dart b/lib/presentation/widgets/mobile_tabs.dart index 8d90d0f..013d37c 100644 --- a/lib/presentation/widgets/mobile_tabs.dart +++ b/lib/presentation/widgets/mobile_tabs.dart @@ -64,7 +64,10 @@ class _MobileBottomMenuState extends ConsumerState { switch (index) { case 0: - context.go(AppRoutes.dashboard); + final dashboardRoute = roleId == 'agent' + ? AppRoutes.partnerPortalDashboard + : AppRoutes.dashboard; + context.go(dashboardRoute); break; case 1: diff --git a/web/index.html b/web/index.html index 7a13bbf..8e7c0fc 100644 --- a/web/index.html +++ b/web/index.html @@ -14,9 +14,9 @@ This is a placeholder for base href that will be replaced by the value of the `--base-href` argument provided to `flutter build`. --> - - - + + +