From 7d684e86b3a8a0ef50c6bbc55ce11252ae9a6f4c Mon Sep 17 00:00:00 2001 From: Venba Team Date: Wed, 2 Apr 2025 15:47:15 +0530 Subject: [PATCH 1/3] User Profile Management --- lib/Screens/itnerary/forex.dart | 1 - lib/Screens/itnerary/taxi.dart | 9 - lib/Screens/plans/create_plans.dart | 10 +- .../plans/dynamic_itinerary_stepper.dart | 49 +- lib/Screens/plans/list_plans.dart | 70 +- lib/Screens/services/apiService.dart | 1 + .../create_user/create_user.dart | 2472 +++++++++++++++++ lib/Screens/userManagement/user_List.dart | 490 ++++ lib/routes/custom_drawer.dart | 169 +- lib/routes/custom_router.dart | 12 +- lib/services/apiService.dart | 72 + lib/utils/auth_utils.dart | 6 + lib/widgets/custom_text_forex.dart | 1 + lib/widgets/custom_user_form.dart | 59 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 40 + pubspec.yaml | 2 + 17 files changed, 3405 insertions(+), 60 deletions(-) create mode 100644 lib/Screens/services/apiService.dart create mode 100644 lib/Screens/userManagement/create_user/create_user.dart create mode 100644 lib/Screens/userManagement/user_List.dart create mode 100644 lib/services/apiService.dart create mode 100644 lib/utils/auth_utils.dart create mode 100644 lib/widgets/custom_user_form.dart diff --git a/lib/Screens/itnerary/forex.dart b/lib/Screens/itnerary/forex.dart index 78b5632..089f515 100644 --- a/lib/Screens/itnerary/forex.dart +++ b/lib/Screens/itnerary/forex.dart @@ -386,7 +386,6 @@ class _ForexScreenState extends State { print("CAsh - $cashAmount- CaRd- $enteredAmount - $calculateAmnt - selectedQuotedAmount - $qouteAmount"); - if (enteredAmount == null || calculateAmnt > qouteAmount!) { errorMessages["deposit_on_card"] = "Amount cannot exceed $qouteAmount"; } else { diff --git a/lib/Screens/itnerary/taxi.dart b/lib/Screens/itnerary/taxi.dart index 326826c..023ffd7 100644 --- a/lib/Screens/itnerary/taxi.dart +++ b/lib/Screens/itnerary/taxi.dart @@ -154,8 +154,6 @@ class _TaxiScreenState extends State { } - - void _clearError(String field) { if (mounted && errorMessages.containsKey(field)) { setState(() { @@ -165,11 +163,6 @@ class _TaxiScreenState extends State { } - - - - - bool isValidData(Map data) { errorMessages.clear(); // Reset errors @@ -327,8 +320,6 @@ class _TaxiScreenState extends State { Column( children: _buildTripType(isDesktop) ) - - ], ), if (isDesktop) diff --git a/lib/Screens/plans/create_plans.dart b/lib/Screens/plans/create_plans.dart index 2625057..80bb76b 100644 --- a/lib/Screens/plans/create_plans.dart +++ b/lib/Screens/plans/create_plans.dart @@ -410,10 +410,6 @@ class _CreateNewPlansState extends State { final String apiUrldata = '$apiUrl/api/getDropdownMaster'; final token = await getToken(); - final userId = "1"; - // final userId = await getUserId(); - - print("SUSRTRT- $userId"); if (token == null) { throw Exception('Token not found. Please log in.'); @@ -456,7 +452,7 @@ class _CreateNewPlansState extends State { final token = await getToken(); - // final userId = await getUserId(); + final userId = await getUserId(); // print("SUSRTRT- $userId"); // @@ -508,7 +504,7 @@ class _CreateNewPlansState extends State { final token = await getToken(); - // final userId = await getUserId(); + final userId = await getUserId(); // print("SUSRTRT- $userId"); // @@ -1270,7 +1266,7 @@ class _CreateNewPlansState extends State { keyboardType: TextInputType.multiline, style: TextStyle(fontSize: 12), enabled: !widget.isViewMode, - decoration: InputDecoration( + decoration: InputDecoration( labelText: "Description", labelStyle: TextStyle(fontSize: 12, color: Colors.grey), floatingLabelBehavior: FloatingLabelBehavior.never, diff --git a/lib/Screens/plans/dynamic_itinerary_stepper.dart b/lib/Screens/plans/dynamic_itinerary_stepper.dart index 84e110a..5cc39bc 100644 --- a/lib/Screens/plans/dynamic_itinerary_stepper.dart +++ b/lib/Screens/plans/dynamic_itinerary_stepper.dart @@ -465,27 +465,51 @@ class _DynamicItineraryState extends State { List _buildOptions() { return [ - _buildOption("Flight"), + _buildOption("Flight", itineraryData["Flight"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Taxi"), + _buildOption("Taxi", itineraryData["Taxi"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Train"), + _buildOption("Train", itineraryData["Train"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Bus"), + _buildOption("Bus", itineraryData["Bus"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Accomodation"), + _buildOption("Accomodation", itineraryData["Accomodation"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Forex"), + _buildOption("Forex", itineraryData["Forex"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Insurance"), + _buildOption("Insurance", itineraryData["Insurance"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Visa"), + _buildOption("Visa", itineraryData["Visa"]?.isNotEmpty ?? false), SizedBox(width: 20), - _buildOption("Miscellaneous"), + _buildOption("Miscellaneous", itineraryData["Miscellaneous"]?.isNotEmpty ?? false), ]; } - Widget _buildOption(String title) { + + // List _buildOptions() { + // return [ + // _buildOption("Flight", itineraryData["Flight"]?.isNotEmpty ?? false), + // _buildOption("Flight"), + // SizedBox(width: 20), + // _buildOption("Taxi"), + // SizedBox(width: 20), + // _buildOption("Train"), + // SizedBox(width: 20), + // _buildOption("Bus"), + // SizedBox(width: 20), + // _buildOption("Accomodation"), + // SizedBox(width: 20), + // _buildOption("Forex"), + // SizedBox(width: 20), + // _buildOption("Insurance"), + // SizedBox(width: 20), + // _buildOption("Visa"), + // SizedBox(width: 20), + // _buildOption("Miscellaneous"), + // ]; + // } + + Widget _buildOption(String title, bool hasData) { return GestureDetector( onTap: () { setState(() { @@ -495,6 +519,11 @@ class _DynamicItineraryState extends State { }, child: Row( children: [ + + if(hasData) + Icon(Icons.circle_notifications,color: Colors.green, size: 10,), + + SizedBox(width: 5), Text(title, style: TextStyle( fontSize: 13, diff --git a/lib/Screens/plans/list_plans.dart b/lib/Screens/plans/list_plans.dart index 078ede5..61908db 100644 --- a/lib/Screens/plans/list_plans.dart +++ b/lib/Screens/plans/list_plans.dart @@ -23,25 +23,67 @@ class ListPlans extends StatefulWidget{ class _ListPlansState extends State{ late Future> futurePlans; + String? userId; + String? token; @override void initState(){ super.initState(); - futurePlans = fetchPlans(); + getToken(); + initializeData(); + + + // futurePlans = fetchPlans(); } + Future initializeData ()async{ + token = await getToken(); + userId = await getUserId(); + + if(token == null || userId == null){ + print("Token or USerId missing"); + return; + } + else{ + setState(() { + futurePlans = fetchPlans(); + }); + } + } + + + + Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + final String? userDataString = prefs.getString('user_data'); + + if(userDataString != null){ + try{ + final Map userData = jsonDecode(userDataString); + return userData["user_id"]?.toString(); + }catch(e){ + return null; + } + } + return null; + } + + Future getToken() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('auth_token'); + } // Fetch API Data Future> fetchPlans() async { - final String apiUrldata = '$apiUrl/api/plans'; + // final String apiUrldata = '$apiUrl/api/plans'; + final String apiUrldata = '$apiUrl/api/plans?user_id=$userId'; + + // final token = await getToken(); - final token = await getToken(); if (token == null) { throw Exception('Token not found. Please log in.'); @@ -68,7 +110,7 @@ class _ListPlansState extends State{ Future > getViewPlan(String planId) async{ final String apiUrldata = '$apiUrl/api/plans/find/$planId'; print("API URL: $apiUrldata"); - final token = await getToken(); + // final token = await getToken(); if (token == null) { throw Exception('Token not found. Please log in.'); @@ -269,9 +311,6 @@ class _ListPlansState extends State{ // }, // ); - - - return Expanded( child: SingleChildScrollView( // scrollDirection: Axis.horizontal, // Outer wrapper for horizontal scrolling @@ -317,7 +356,22 @@ class _ListPlansState extends State{ DataCell(Text(plan.tripType)), DataCell(Text(plan.costCenter)), DataCell(Text(plan.isBillable)), - DataCell(Text(plan.status)), + DataCell( + Container( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), // Padding for better look + decoration: BoxDecoration( + color: plan.status == "Active" ? Colors.green.shade50 : Colors.grey.shade50, // Background color + borderRadius: BorderRadius.circular(10), // Rounded corners + ), + child: Text( + plan.status, + style: TextStyle( + color: plan.status == "Active" ? Colors.green : Colors.grey, // Text color + fontWeight: FontWeight.bold, // Optional: Make text bold + ), + ), + ), + ), DataCell( Row( children:[ diff --git a/lib/Screens/services/apiService.dart b/lib/Screens/services/apiService.dart new file mode 100644 index 0000000..b9f78bb --- /dev/null +++ b/lib/Screens/services/apiService.dart @@ -0,0 +1 @@ +// TODO Implement this library. \ No newline at end of file diff --git a/lib/Screens/userManagement/create_user/create_user.dart b/lib/Screens/userManagement/create_user/create_user.dart new file mode 100644 index 0000000..54e1384 --- /dev/null +++ b/lib/Screens/userManagement/create_user/create_user.dart @@ -0,0 +1,2472 @@ +import 'dart:convert'; +import 'dart:html' as html; +import 'dart:typed_data'; // Import for Uint8List + +import 'package:bcrypt/bcrypt.dart'; +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:responsive_builder/responsive_builder.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../config/apiUrl.dart'; +import '../../../routes/custom_appBar.dart'; +import '../../../routes/custom_drawer.dart'; +import '../../../services/apiService.dart'; +import '../../../widgets/custom_text_field.dart'; +import '../../../widgets/custom_text_forex.dart'; +import '../../../widgets/custom_user_form.dart'; + + + + +class CreateUserForm extends StatefulWidget{ + @override + _CreateUserFormState createState() => _CreateUserFormState(); +} + +class _CreateUserFormState extends State{ + final ApiService apiService = ApiService(); + + String? userId; + + String? token; + + // Map to store controllers dynamically + final Map controllers = {}; + bool isViewMode = false; + bool isEditProfile = true; + // late final List? apiCountryData ; + + late List? apiCountryData; + + + late List? apiUserData; + Map? apiselectedUser; + + + late List userList; + late Future> futureUsers; + + List? resolvedUserData; + late List userIdsApi; + late Map userMap; + + + Map errorMessages = {}; + + + String? selectedCountry; + String? selectedGender; + String? selectedUserType; + String? selectedRole; + String? selectedLevel; + String? selectedDepartment; + String? selectedFirstApprover; + String? selectedSecondApprover; + String? selectedThirdApprover; + + String? selectedFileNames; + Uint8List? passportDocumentBytes; + String? base64PDF; + + + List dataHeader = [ + "Fname", "Lname", "email", "password", "dob","gender","mobileNumber", "alternateMobile", "address", + "postalCode","country","passportNumber", "placeOfIssue", "passportDoc","userType","roleId","deptId", + "levelId", "firstApproval", "secondApproval", "thirdApproval","employeeCode","dateOfIssue","dateOfExpiry", + "changePassword" + ]; + + Map get userDetials{ + + Map data = { + "first_name" : controllers["Fname"]?.text, + "last_name" : controllers["Lname"]?.text, + "email" : controllers["email"]?.text, + "password" : controllers["password"]?.text, + "mobile_no" : controllers["mobileNumber"]?.text, + "alternate_mobile_no": controllers["alternateMobile"]?.text, + "date_of_birth": controllers["dob"]?.text, + "address" : controllers["address"]?.text, + "gender" : selectedGender, + "postal_code" : controllers["postalCode"]?.text, + "country" : selectedCountry, + "employee_code": controllers["employeeCode"]?.text, + + "user_type" : selectedUserType, + "role_id" : selectedRole, + "department_id": selectedDepartment, + "level_id" : selectedLevel, + + "first_approver": selectedFirstApprover, + "second_approver": selectedSecondApprover, + "third_approver": selectedThirdApprover, + "passport_number": controllers["passportNumber"]?.text, + "place_of_issue": controllers["placeOfIssue"]?.text, + "passport_document": selectedFileNames, + + "date_of_issue": controllers["dateOfIssue"]?.text, + "date_of_expiry": controllers["dateOfExpiry"]?.text, + "created_by": userId, + "is_active": "1", + "passport_fileData": base64PDF, + }; + return data; + } + + void updateData() { + // Ensure apiselectedUser is not null before printing + if (apiselectedUser != null) { + print("API Selected User Has Data - $apiselectedUser"); + setState(() { // ✅ Wrap in setState to update the UI + + controllers["Fname"]?.text = apiselectedUser?["first_name"] ?? ""; + controllers["Lname"]?.text = apiselectedUser?["last_name"] ?? ""; + controllers["email"]?.text = apiselectedUser?["email"] ?? ""; + controllers["password"]?.text = ""; + // controllers["password"]?.text = apiselectedUser?["password"] ?? ""; + controllers["mobileNumber"]?.text = apiselectedUser?["mobile_no"] ?? ""; + controllers["alternateMobile"]?.text = apiselectedUser?["alternate_mobile_no"] ?? ""; + controllers["dob"]?.text = apiselectedUser?["date_of_birth"] ?? ""; + controllers["address"]?.text = apiselectedUser?["address"] ?? ""; + controllers["postalCode"]?.text = apiselectedUser?["postal_code"] ?? ""; + controllers["employeeCode"]?.text = apiselectedUser?["employee_code"] ?? ""; + + controllers["passportNumber"]?.text = apiselectedUser?["passport_number"] ?? ""; + controllers["placeOfIssue"]?.text = apiselectedUser?["place_of_issue"] ?? ""; + + controllers["dateOfIssue"]?.text = apiselectedUser?["date_of_issue"] ?? ""; + controllers["dateOfExpiry"]?.text = apiselectedUser?["date_of_expiry"] ?? ""; + + + selectedCountry = apiselectedUser?["country"]?.toString() ?? ""; + selectedGender = apiselectedUser?["gender"]?.toString().trim() ?? ""; + + selectedFileNames = apiselectedUser?["passport_document"]?.toString().trim() ?? ""; + selectedUserType = apiselectedUser?["user_type"]?.toString().trim() ?? ""; + selectedRole = apiselectedUser?["role_id"]?.toString().trim() ?? ""; + selectedDepartment = apiselectedUser?["department_id"]?.toString().trim() ?? ""; + selectedLevel = apiselectedUser?["level_id"]?.toString().trim() ?? ""; + + selectedFirstApprover = apiselectedUser?["first_approver"]?.toString() ?? ""; + selectedSecondApprover = apiselectedUser?["second_approver"]?.toString() ?? ""; + selectedThirdApprover = apiselectedUser?["third_approver"]?.toString() ?? ""; + + + print("Updated selectedGender: $selectedGender"); // Debugging + }); + + } else { + print("API Selected User Has Data - No data available yet"); + } + } + + + @override + void initState() { + super.initState(); + WidgetsFlutterBinding.ensureInitialized(); + + // apiCountryData = extraData['apiCountryData']; // Extract apiCountryData + // futureUsers = extraData['apiUserData']; // Extract futureUsers (Future>) + apiCountryData = null; + apiUserData = null; + apiselectedUser = null; + + // Delay accessing context until the widget is fully initialized + // WidgetsBinding.instance.addPostFrameCallback((_) { + // setState(() { + // apiCountryData = (GoRouterState.of(context).extra as Map)['apiCountryData']; + // apiUserData = (GoRouterState.of(context).extra as Map)['apiUserData']; + // apiselectedUser = (GoRouterState.of(context).extra as Map)['selectedUser']; + // + // + // userList = apiUserData ?? []; + // userMap = { + // for (var user in userList) + // user['user_id'] as String: "${user['first_name']} ${user['last_name']}" + // }; + // + // userIdsApi = userMap.keys.toList(); + // + // + // }); + // }); + + + + + + // Initialize controllers for each field + + + WidgetsBinding.instance.addPostFrameCallback((_) async{ + final extraData = GoRouterState.of(context).extra as Map?; + + + + if (extraData != null) { + print("extraData: ${extraData['selectedUser']}"); + + setState(() { + // apiCountryData = extraData['apiCountryData']; + // apiUserData = extraData['apiUserData']; + // + // userList = apiUserData ?? []; + // userMap = { + // for (var user in userList) + // user['user_id'].toString(): "${user['first_name']} ${user['last_name']}" + // }; + // userIdsApi = userMap.keys.toList(); + + // Handle selectedUser as a Map (not a List) + apiselectedUser = extraData['selectedUser'] as Map?; // Cast it as a Map + isViewMode = extraData['isViewMode'] ?? false; + isEditProfile = extraData['isEditProfile'] ?? false; + + }); + + // Add another post-frame callback to check after setState + await Future.delayed(Duration(milliseconds: 100)); // Optional delay to ensure UI has updated + updateData(); + } + }); + + + + for(var field in dataHeader) { + controllers[field] = TextEditingController(); + } + + initializeData(); + fetchCountries(); + fetchUsers(); + + } + + Future fetchCountries() async { + try { + List countries = await apiService.fetchCountryList(); + setState(() { + apiCountryData = countries; + }); + } catch (e) { + print('Error fetching country list: $e'); + } + } + + Future fetchUsers() async { + try { + List users = await apiService.fetchUsers(); + setState(() { + apiUserData = users; + + userList = apiUserData ?? []; + userMap = { + for (var user in userList) + user['user_id'].toString(): "${user['first_name']} ${user['last_name']}" + }; + userIdsApi = userMap.keys.toList(); + }); + } catch (e) { + print('Error fetching country list: $e'); + } + } + + + + void printFormData() { + for (var field in dataHeader) { + print("$field: ${controllers[field]?.text}"); + } + } + + Future initializeData ()async{ + token = await getToken(); + userId = await getUserId(); + + if(token == null || userId == null){ + print("Token or USerId missing"); + print("Retrieved Token: $token"); + print("Retrieved UserId: $userId"); + return; + } + else{ + setState(() { + }); + } + } + + Future getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('auth_token'); + } + + + // Future getUserId() async { + // final prefs = await SharedPreferences.getInstance(); + // return prefs.getString('userId'); + // } + + + Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + final userData = prefs.getString('user_data'); + + if (userData != null) { + final decodedData = jsonDecode(userData); + + // Return the user_id from the decoded data + return decodedData['user_id']?.toString(); + } + return null; + } + + + + @override + void dispose() { + // Dispose all controllers to prevent memory leaks + for (var controller in controllers.values) { + controller.dispose(); + } + super.dispose(); + } + + void handleSubmit(){ + print("USR Detail Submit"); + printFormData(); + + Map data = userDetials; + + if (!isValidData(data)) { + print("Validation Failed: Required fields are missing."); + setState(() {}); + return; // Stop execution if validation fails + }else { + print("USERDETAILS : $userDetials"); + createUserData(userDetials); + } + + + } + + + bool isValidData(Map data) { + errorMessages.clear(); // Reset errors + + // Required fields that must not be empty + List requiredFields = ["first_name", "last_name","email","password","mobile_no"]; + + // Check validation for each field + for (String field in requiredFields) { + if (data[field] == null || data[field].toString().trim().isEmpty) { + errorMessages[field] = "Required"; + } + } + + // Mobile number validation for both mobile_no and alternate_mobile_no + if (data["mobile_no"] != null && data["mobile_no"].toString().isNotEmpty) { + if (!RegExp(r"^\d{10}$").hasMatch(data["mobile_no"].toString())) { + errorMessages["mobile_no"] = "Enter 10 digits"; // Invalid mobile number format + } + } + + if (data["alternate_mobile_no"] != null && data["alternate_mobile_no"].toString().isNotEmpty) { + if (!RegExp(r"^\d{10}$").hasMatch(data["alternate_mobile_no"].toString())) { + errorMessages["alternate_mobile_no"] = "Enter 10 digits"; // Invalid mobile number format + } + } + + // Email validation + if (data["email"] != null && data["email"].toString().isNotEmpty) { + if (!RegExp(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").hasMatch(data["email"].toString())) { + errorMessages["email"] = "Invalid email format"; // Invalid email format + } + } + return errorMessages.isEmpty; // Valid if there are no errors + } + + void _clearError(String field) { + if (mounted && errorMessages.containsKey(field)) { + setState(() { + errorMessages.remove(field); + }); + } + } + + + void pickPDFWeb() { + html.FileUploadInputElement uploadInput = html.FileUploadInputElement(); + uploadInput.accept = '.pdf'; + uploadInput.click(); + + uploadInput.onChange.listen((e) { + final file = uploadInput.files!.first; + final reader = html.FileReader(); + + reader.readAsArrayBuffer(file); + reader.onLoadEnd.listen((event) { + print('File picked: ${file.name}'); + print('File size: ${file.size} bytes'); + + + // Update the state with the selected file name + setState(() { + selectedFileNames = file.name; // Store only one file name + passportDocumentBytes = reader.result as Uint8List; // Store file data + + // 🔹 Convert file to Base64 + base64PDF = base64Encode(passportDocumentBytes! as List); + // File size check: Ensure the file size does not exceed 3MB + int maxFileSize = 3 * 1024 * 1024; // 3MB in bytes + + if (file.size > maxFileSize) { + print('Error: File size exceeds 3MB'); + // You can show an error message here if necessary + // For example: + // showError('File size cannot exceed 3MB'); + return; + } + + print('File size: ${file.size} bytes'); + print('Base64 Data: $base64PDF'); // Debugging + }); + + + + }); + }); + } + + Future createUserData(Map userData) async { + + bool isUpdating = apiselectedUser != null && apiselectedUser!.isNotEmpty; + final String apiUrldata = isUpdating? + '$apiUrl/api/users/update/${apiselectedUser?["user_id"]}' + :'$apiUrl/api/users/create'; + + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + + // Add user_id only if updating + if (isUpdating) { + userData['user_id'] = apiselectedUser?["user_id"]; + } + + try { + final response = isUpdating + ? await http.put( + Uri.parse(apiUrldata), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + body: jsonEncode(userData), + ) + : await http.post( + Uri.parse(apiUrldata), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + body: jsonEncode(userData), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + print("Plan submitted successfully!"); + print("Response: ${response.body}"); + context.go('/listUser'); + } else { + print("Failed to submit plan. Status: ${response.statusCode}"); + print("Error: ${response.body}"); + } + } catch (e) { + print(" Error submitting plan: $e"); + } + } + + void handleChangePassword(String enteredPassword) { + var hashedPassword = apiselectedUser?["password"]; // Get stored bcrypt hash + + print("HAshPAssword - $hashedPassword "); + print("enteredPassword - $enteredPassword "); + + if (hashedPassword != null && hashedPassword.isNotEmpty) { + bool isMatch = BCrypt.checkpw(enteredPassword, hashedPassword); // Compare passwords + + setState(() { // ✅ Ensure UI updates + if (isMatch) { + errorMessages["password"] = "New password is not similar to old password"; + print(" Password match!"); + } else { + print(" Password NOT match!"); + errorMessages.remove("password"); // Clear error if password is different + } + }); + } else { + print("No stored password found."); + } + } + + + @override + Widget build(BuildContext context){ + return ResponsiveBuilder(builder: (context, sizingInfo) { + bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop; + + return Scaffold( + appBar: isDesktop ? null : const CustomAppBar(title:'Create User '), + drawer: isDesktop ? null : CustomDrawer(isDesktop: false), + body: + Container( + color: Colors.white, + child: Row( + children: [ + if(isDesktop) + CustomDrawer(isDesktop: true), + // const Expanded(child: Center(child: Text("User Details Content"))), + Expanded(child: buildUserTable(isDesktop)), + ], + ), + ), + ); + }); + + } + + Widget buildUserTable(bool isDesktop){ + return Container( + margin: const EdgeInsets.only(left: 10.0, right: 15.0, top: 10.0,bottom: 10.0), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFFF7F7FB),width: 3.5) + + ), + + child: Column( + children: [ + + Container( + color: Color(0xFFF7F7FB), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row(children: [ + + Text(apiselectedUser != null ? "Profile": "New User", + style: TextStyle(fontSize: 18,fontWeight: FontWeight.w200, color: Colors.black ),), + if(isEditProfile) + IconButton( + icon: Icon(Icons.edit, color: Colors.blueAccent,size: 18,), + onPressed:(){ + setState(() { + isViewMode = !isViewMode; + }); + } + ) + ], + ), + + ), + ), + + + SizedBox(height: 15 ,), + + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: _buildUserDetails(isDesktop), // Ensure this returns a scrollable widget + ), + ), + + Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(18.0), + child: isDesktop + ? Row( + mainAxisAlignment: MainAxisAlignment.end, + children: _buildSubmit(isDesktop),) + :Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _buildSubmit(isDesktop),) + ), + ) + + ], + ), + ); + + } + + Widget _buildUserDetails(bool isDesktop){ + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(Icons.filter_1_outlined, color: Color(0xFF8B8FB2),size: 18,), + SizedBox(width: 10,), + Text("Personal Details", style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold, + // color: Colors.black, + color: Color(0xFF8B8FB2) + + ), + ) + ],), + SizedBox(height: 20,), + + Padding( + padding: const EdgeInsets.only(left:16.0), + // padding: const EdgeInsets.all(8.0), + // color: Colors.grey, + child: SizedBox( + width: isDesktop? MediaQuery.of(context).size.width *0.7: null, + // height: isDesktop? null :MediaQuery.of(context).size.height , + child: isDesktop + ? Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: _buildFirstRowLeftColumn(isDesktop)), + SizedBox(width: 20), + Expanded(child: _buildFirstRowRightColumn(isDesktop)), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFirstRowLeftColumn(isDesktop), + SizedBox(height: 1), + _buildFirstRowRightColumn(isDesktop), + ], + ), + ), + ), + + + ],), + + + + ], + ), + ), + + // SizedBox( + // child: Expanded( + // child: Container( + // margin: const EdgeInsets.only(left: 10.0, right: 15.0), + // child: Divider( + // color: Color(0xFF8B8FB2), + // thickness: 0.5, + // height: 50, + // ), + // ), + // ), + // ), + + SizedBox( + width: double.infinity, // Ensures the divider takes full width + child: Stack( + alignment: Alignment.center, // Centers the icon + children: [ + Divider( + color: Color(0xFF8B8FB2), + thickness: 0.5, + height: 50, + ), + Container( + // padding: EdgeInsets.all(4), + color: Colors.white, // Background to avoid overlapping + child: Row( + mainAxisSize: MainAxisSize.min, // Prevents row from taking full width + children: [ + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + ], + ), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(Icons.filter_2_outlined, color: Color(0xFF8B8FB2),size: 18,), + SizedBox(width: 10,), + Text("Passport Details", style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold, + // color: Colors.black, + color: Color(0xFF8B8FB2) + + ), + ) + ],), + SizedBox(height: 20,), + + Padding( + padding: const EdgeInsets.only(left:16.0), + child: SizedBox( + width: isDesktop? MediaQuery.of(context).size.width *0.7: null, + // height: isDesktop? null :MediaQuery.of(context).size.height , + child: isDesktop + ? Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildSecondRowLeftColumn(isDesktop)), + SizedBox(width: 20), + Expanded(child: _buildSecondRowRightColumn(isDesktop)), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + _buildSecondRowLeftColumn(isDesktop), + SizedBox(height: 1), + _buildSecondRowRightColumn(isDesktop), + ], + ), + ), + ), + ],), + ], + ), + ), + + SizedBox( + width: double.infinity, // Ensures the divider takes full width + child: Stack( + alignment: Alignment.center, // Centers the icon + children: [ + Divider( + color: Color(0xFF8B8FB2), + thickness: 0.5, + height: 50, + ), + Container( + // padding: EdgeInsets.all(4), + color: Colors.white, // Background to avoid overlapping + child: Row( + mainAxisSize: MainAxisSize.min, // Prevents row from taking full width + children: [ + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + ], + ), + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(Icons.filter_3_outlined, color: Color(0xFF8B8FB2),size: 18,), + SizedBox(width: 10,), + Text("Employee Organization Details", style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold, + // color: Colors.black, + color: Color(0xFF8B8FB2) + + ), + ) + ],), + SizedBox(height: 20,), + + Padding( + padding: const EdgeInsets.only(left:16.0), + child: SizedBox( + width: isDesktop? MediaQuery.of(context).size.width *0.7: null, + // height: isDesktop? null :MediaQuery.of(context).size.height , + child: isDesktop + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildThirdRowLeftColumn(isDesktop)), + SizedBox(width: 20), + Expanded(child: _buildThirdRowRightColumn(isDesktop)), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildThirdRowLeftColumn(isDesktop), + SizedBox(height: 1), + _buildThirdRowRightColumn(isDesktop), + ], + ), + ), + ), + + ],), + + + + ], + ), + ), + + SizedBox( + width: double.infinity, // Ensures the divider takes full width + child: Stack( + alignment: Alignment.center, // Centers the icon + children: [ + Divider( + color: Color(0xFF8B8FB2), + thickness: 0.5, + height: 50, + ), + Container( + // padding: EdgeInsets.all(4), + color: Colors.white, // Background to avoid overlapping + child: Row( + mainAxisSize: MainAxisSize.min, // Prevents row from taking full width + children: [ + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(Icons.filter_4_outlined, color: Color(0xFF8B8FB2),size: 18,), + SizedBox(width: 10,), + Text("Approver", style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold, + // color: Colors.black, + color: Color(0xFF8B8FB2) + + ), + ) + ],), + SizedBox(height: 20,), + + + Padding( + padding: const EdgeInsets.only(left:16.0), + child: SizedBox( + width: isDesktop? MediaQuery.of(context).size.width *0.7: null, + // height: isDesktop? null :MediaQuery.of(context).size.height , + child: isDesktop + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildForthRowLeftColumn(isDesktop)), + SizedBox(width: 20), + Expanded(child: _buildForthRowRightColumn(isDesktop)), + SizedBox(width: 20), + Expanded(child: _buildForthRowLastColumn(isDesktop)), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildForthRowLeftColumn(isDesktop), + SizedBox(height: 1), + _buildForthRowRightColumn(isDesktop), + SizedBox(height: 1), + _buildForthRowLastColumn(isDesktop), + ], + ), + ), + ), + + + + ],), + + ], + ), + ), + + SizedBox( + width: double.infinity, // Ensures the divider takes full width + child: Stack( + alignment: Alignment.center, // Centers the icon + children: [ + Divider( + color: Color(0xFF8B8FB2), + thickness: 0.5, + height: 50, + ), + Container( + // padding: EdgeInsets.all(4), + color: Colors.white, // Background to avoid overlapping + child: Row( + mainAxisSize: MainAxisSize.min, // Prevents row from taking full width + children: [ + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + Icon(Icons.keyboard_arrow_right_outlined, color: Colors.blue, size: 28), SizedBox(width: 1), + ], + ), + ), + ], + ), + ), + ], + ); + + } + + //--------------------------------- Personal Details --------------------------- + + Widget _buildFirstRowLeftColumn(bool isDesktop){ + + DateTime? _selectedDateOfBirth; + + Future _selectCheckDateOfBirth(BuildContext context) async { + DateTime now = DateTime.now(); + DateTime today = DateTime(now.year, now.month, now.day); + + DateTime? pickedDate = await showDatePicker( + context: context, + + + initialDate: _selectedDateOfBirth != null && _selectedDateOfBirth!.isAfter(today) + ? _selectedDateOfBirth! + : today, + firstDate: today, + lastDate: DateTime(2100), + ); + + if (pickedDate != null && pickedDate != _selectedDateOfBirth) { + setState(() { + _selectedDateOfBirth = pickedDate; + controllers["dob"]?.text = DateFormat('yyyy-MM-dd').format(pickedDate); + }); + } + } + + + return Container( + // color: Colors.yellow, + color: Colors.white, + child: Expanded( // Allow first column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("First Name", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["Fname"], + enabled: !isViewMode, + onChanged: (value) {_clearError("first_name");}, + decoration: InputDecoration( + labelText: "FirstName", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + if (errorMessages["first_name"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["first_name"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Last Name", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["Lname"], + enabled: !isViewMode, + onChanged: (value) {_clearError("last_name");}, + decoration: InputDecoration( + labelText: "LastName", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + if (errorMessages["last_name"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["last_name"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Gender", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: DropdownButtonFormField( + value: selectedGender, + onChanged: isViewMode ? null : (String? newValue) { + setState(() { + selectedGender = newValue; + print("selectedGender - $selectedGender"); + }); + }, + decoration: InputDecoration( + border: InputBorder.none, + enabled: !isViewMode, // Disables input when in view mode + ), + style: TextStyle(fontSize: 12), + items: [ + DropdownMenuItem(value: "Male", child: Text("Male")), + DropdownMenuItem(value: "Female", child: Text("Female")), + ], + hint : Text( + selectedGender ?? "Select Gender", + style: TextStyle(fontSize: 12),), + ), + + // child: DropdownSearch( + // selectedItem: selectedGender, + // // key: ValueKey(selectedGender), + // popupProps: PopupProps.menu( + // fit: FlexFit.loose, // Allows flexible height + // constraints: BoxConstraints(maxHeight: 250), + // + // ), + // items: ["Male", "Female", "Others"], + // dropdownDecoratorProps: DropDownDecoratorProps( + // dropdownSearchDecoration: InputDecoration( + // border: InputBorder.none, + // contentPadding: EdgeInsets.symmetric(horizontal: 1,), + // ), + // ), + // dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + // alignment: Alignment.centerLeft, + // child: Text( + // selectedItem ?? "Select", + // style: TextStyle(fontSize: 12), + // ), + // ), + // onChanged: isViewMode + // ? null + // : (String? newValue) { + // setState(() { + // // Find the country_code based on selected country_name + // selectedGender = newValue; + // print("selectedGender - $selectedGender"); + // // if (selectedCountry!.isNotEmpty) { + // // errorMessages.remove("country_code"); + // // } + // }); + // }, + // + // + // ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Date of Birth", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: GestureDetector( + onTap: isViewMode ? null :()async{ + await _selectCheckDateOfBirth(context); + if (controllers["dob"]!.text.isNotEmpty) { + setState(() { + // errorMessages.remove("start_date"); + }); + } + + }, + child: AbsorbPointer( + child: TextField( + controller: controllers["dob"], + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + labelText: "Select Date", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + suffixIcon: Icon(Icons.calendar_today, + size: 16, + color: Color(0xFF8B8FB2) + ), + ), + ), + ), + ), + + ), + ), + ], + ), + ], + ), + + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Email", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["email"], + onChanged: (value) {_clearError("email");}, + enabled: !isViewMode, + decoration: InputDecoration( + labelText: "Email", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + if (errorMessages["email"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["email"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + ], + ), + + SizedBox(height: 3), + + + apiselectedUser != null? + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Change Password", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["password"], + enabled: !isViewMode, + onChanged: (value) { + _clearError("changePassword"); + handleChangePassword(value); + }, + decoration: InputDecoration( + labelText: "Change Password", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + if (errorMessages["password"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["password"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + ], + ): + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Password", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["password"], + enabled: !isViewMode, + onChanged: (value) {_clearError("password");}, + decoration: InputDecoration( + labelText: "Password", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + if (errorMessages["password"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["password"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + ], + ), + + + if(!isDesktop) SizedBox(height: 3,), + ], + ), + ), + ); + } + + Widget _buildFirstRowRightColumn(bool isDesktop){ + + late Map countryMap; // Mapping country_code -> country_name + late List countryCodes; // List of country codes + List countryList = apiCountryData ?? []; + + countryList = apiCountryData ?? []; + + // Map country codes to country names + countryMap = { + for (var item in countryList) item['country_code'] as String: item['country_name'] as String + }; + + // Extract only country codes for processing + countryCodes = countryMap.keys.toList(); + + selectedCountry ??= null; + + return Container( + // color: Colors.yellow, + color: Colors.white, + child: Expanded( // Allow second column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Mobile Number", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["mobileNumber"], + enabled: !isViewMode, + onChanged: (value) {_clearError("mobile_no");}, + keyboardType: TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')), // Allow only positive numbers with optional decimal + ], + decoration: InputDecoration( + labelText: "Mobile Number", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + if (errorMessages["mobile_no"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["mobile_no"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Alternate Mobile Number", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["alternateMobile"], + enabled: !isViewMode, + keyboardType: TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')), // Allow only positive numbers with optional decimal + ], + decoration: InputDecoration( + labelText: "Alternate Number", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + if (errorMessages["alternate_mobile_no"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["alternate_mobile_no"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Address", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 110, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["address"], + enabled: !isViewMode, + maxLines: 4, + keyboardType: TextInputType.multiline, + decoration: InputDecoration( + labelText: "Address", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + // contentPadding: EdgeInsets.symmetric(vertical: 16), + contentPadding: EdgeInsets.symmetric(vertical: 4), + ), + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Postal Code", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["postalCode"], + enabled: !isViewMode, + keyboardType: TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')), // Allow only positive numbers with optional decimal + ], + decoration: InputDecoration( + labelText: "Postal Code", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Country", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: DropdownSearch( + selectedItem: countryMap[selectedCountry], + enabled: !isViewMode, + popupProps: PopupProps.menu( + showSearchBox: true, // Enables search functionality + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Search Country...", + contentPadding: EdgeInsets.symmetric(horizontal: 10), + ), + ), + + ), + items: countryMap.values.toList(), + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select Country", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: isViewMode ? null: (String? newValue) { + setState(() { + // Find the country_code based on selected country_name + selectedCountry = countryMap.entries + .firstWhere((entry) => entry.value == newValue) + .key; + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + ), + ], + ), + ], + ), + + + + if(!isDesktop) SizedBox(height: 3,), + ], + ), + ), + ); +} + + + + //--------------------------------- Passport Details ----------------------------- + Widget _buildSecondRowLeftColumn(bool isDesktop){ + return Container( + color: Colors.white, + child: Expanded( // Allow first column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Passport Number", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["passportNumber"], + enabled: !isViewMode, + decoration: InputDecoration( + labelText: "Passport Number", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 3,), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Passport Document", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: GestureDetector( + onTap: pickPDFWeb, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + alignment: Alignment.centerLeft, + + child: Text( + selectedFileNames?? "Select Document", + style: TextStyle(fontSize: 12, color: Colors.black), + overflow: TextOverflow.ellipsis, // Prevents overflow issues + ), + ), + ), + ), + ), + + + + // CustomTextFieldUserWrapper( + // isFocused: false, + // isDesktop: isDesktop, + // child: SizedBox( + // height: 40, + // child: TextField( + // style: TextStyle(fontSize: 12), + // controller: controllers["passportDoc"], + // decoration: InputDecoration( + // labelText: "Select Document", + // labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + // floatingLabelBehavior: FloatingLabelBehavior.never, + // border: InputBorder.none, + // contentPadding: EdgeInsets.symmetric(vertical: 16), + // ), + // ), + // ), + // ), + ], + ), + ], + ), + if(!isDesktop) SizedBox(height: 3,), + ], + ), + ), + ); + + } + + Widget _buildSecondRowRightColumn(bool isDesktop){ + + DateTime? _selectedDateOfIssue; + + Future _selectCheckDateOfIssue(BuildContext context) async { + DateTime now = DateTime.now(); + DateTime today = DateTime(now.year, now.month, now.day); + + DateTime? pickedDate = await showDatePicker( + context: context, + + + initialDate: _selectedDateOfIssue != null && _selectedDateOfIssue!.isAfter(today) + ? _selectedDateOfIssue! + : today, + firstDate: today, + lastDate: DateTime(2100), + ); + + if (pickedDate != null && pickedDate != _selectedDateOfIssue) { + setState(() { + _selectedDateOfIssue = pickedDate; + controllers["dateOfIssue"]?.text = DateFormat('yyyy-MM-dd').format(pickedDate); + }); + } + } + + DateTime? _selectedDateOfExpiry; + + Future _selectCheckDateOfExpiry(BuildContext context) async { + DateTime now = DateTime.now(); + DateTime today = DateTime(now.year, now.month, now.day); + + DateTime? pickedDate = await showDatePicker( + context: context, + + + initialDate: _selectedDateOfExpiry != null && _selectedDateOfExpiry!.isAfter(today) + ? _selectedDateOfExpiry! + : today, + firstDate: today, + lastDate: DateTime(2100), + ); + + if (pickedDate != null && pickedDate != _selectedDateOfExpiry) { + setState(() { + _selectedDateOfExpiry = pickedDate; + controllers["dateOfExpiry"]?.text = DateFormat('yyyy-MM-dd').format(pickedDate); + }); + } + } + + + return Container( + color: Colors.white, + child: Expanded( // Allow second column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Place of Issue", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["placeOfIssue"], + enabled: !isViewMode, + decoration: InputDecoration( + labelText: "Place Of Issue", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 3,), + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Date of Issue", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: GestureDetector( + onTap: isViewMode? null : ()async{ + await _selectCheckDateOfIssue(context); + if (controllers["dateOfIssue"]!.text.isNotEmpty) { + setState(() { + // errorMessages.remove("start_date"); + }); + } + + }, + child: AbsorbPointer( + child: TextField( + controller: controllers["dateOfIssue"], + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + labelText: "Select Date", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + suffixIcon: Icon(Icons.calendar_today, + size: 16, + color: Color(0xFF8B8FB2) + ), + ), + ), + ), + ), + + + + ), + ), + ], + ), + ], + ), + SizedBox(height: 3,), + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Date of Expiry", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: GestureDetector( + onTap: isViewMode? null : ()async{ + await _selectCheckDateOfExpiry(context); + if (controllers["dateOfExpiry"]!.text.isNotEmpty) { + setState(() { + // errorMessages.remove("start_date"); + }); + } + + }, + child: AbsorbPointer( + child: TextField( + controller: controllers["dateOfExpiry"], + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + labelText: "Select Date", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + suffixIcon: Icon(Icons.calendar_today, + size: 16, + color: Color(0xFF8B8FB2) + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if(!isDesktop) SizedBox(height: 3,), + + ], + ), + ), + ); + } + + +//----------------------------------- Employee Organization Details ------------------ + Widget _buildThirdRowLeftColumn(bool isDesktop){ + return Container( + color: Colors.white, + child: Expanded( // Allow first column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Employee Code", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + controller: controllers["employeeCode"], + enabled: !isViewMode, + decoration: InputDecoration( + labelText: "Employee Code", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + ], + ), + ], + ), + + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("User Type", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: DropdownSearch( + selectedItem: selectedUserType, + enabled: !isViewMode, + popupProps: PopupProps.menu( + // showSearchBox: true, + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + + ), + items: ["Admin", "Manager", "HR"], + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: (String? newValue) { + setState(() { + // Find the country_code based on selected country_name + selectedUserType = newValue; + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + ), + ], + ), + ], + ), + if(!isDesktop) + SizedBox(height: 3), + ], + ), + ), + ); + } + + Widget _buildThirdRowRightColumn(bool isDesktop){ + return Container( + color: Colors.white, + child: Expanded( // Allow second column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Role ", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: DropdownSearch( + selectedItem: selectedRole, + enabled: !isViewMode, + popupProps: PopupProps.menu( + // showSearchBox: true, + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + ), + items: ["Role 1", "Role 2", "Role 3"], + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: (String? newValue) { + setState(() { + // Find the country_code based on selected country_name + selectedRole = newValue; + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Level", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: DropdownSearch( + selectedItem: selectedLevel, + enabled: !isViewMode, + popupProps: PopupProps.menu( + // showSearchBox: true, + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + + ), + items: ["Level 1", "Level 2", "Level 3"], + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: (String? newValue) { + setState(() { + // Find the country_code based on selected country_name + selectedLevel = newValue; + + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 3), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Department", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + child: SizedBox( + height: 40, + child: DropdownSearch( + selectedItem: selectedDepartment, + enabled: !isViewMode, + popupProps: PopupProps.menu( + // showSearchBox: true, + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + + ), + items: ["Dep1", "Dep2", "Dep3"], + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: (String? newValue) { + setState(() { + // Find the country_code based on selected country_name + selectedDepartment = newValue; + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + ), + ], + ), + ], + ), + + ], + ), + ), + ); + } + + + +//----------------------------------- Approver --------------------------------------- + + Widget _buildForthRowLeftColumn(bool isDesktop){ + + + return Container( + color: Colors.white, + child: Expanded( // Allow first column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("First Approver", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + width: isDesktop? MediaQuery.of(context).size.width * 0.2 : MediaQuery.of(context).size.width * 0.8, + child: SizedBox( + height: 40, + child: apiUserData == null + ? Center(child: Transform.scale(scale: 0.5, child: CircularProgressIndicator(),),) + : DropdownSearch( + selectedItem: userMap[selectedFirstApprover], + enabled: !isViewMode, + popupProps: PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Search User...", + contentPadding: EdgeInsets.symmetric(horizontal: 10), + ), + ), + ), + items: apiUserData!.map((user) { + return "${user['first_name']} ${user['last_name']}"; + }).toList(), + + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + + + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: (String? newValue) { + setState(() { + + selectedFirstApprover = userMap.entries + .firstWhere((entry) => entry.value == newValue) + .key; + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + ), + ], + ), + ], + ), + ], + ), + ), + + ); + } + + Widget _buildForthRowRightColumn(bool isDesktop){ + return Container( + color: Colors.white, + child: Expanded( // Allow second column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Second Approver", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + width: isDesktop? MediaQuery.of(context).size.width * 0.2 : MediaQuery.of(context).size.width * 0.8, + child: SizedBox( + height: 40, + child: apiUserData == null + ? Center(child: Transform.scale(scale: 0.5, child: CircularProgressIndicator(),),) + : DropdownSearch( + selectedItem: userMap[selectedSecondApprover], + enabled: !isViewMode, + popupProps: PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Search User...", + contentPadding: EdgeInsets.symmetric(horizontal: 10), + ), + ), + ), + items: apiUserData!.map((user) { + return "${user['first_name']} ${user['last_name']}"; + }).toList(), + + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + + + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: (String? newValue) { + setState(() { + + selectedSecondApprover = userMap.entries + .firstWhere((entry) => entry.value == newValue) + .key; + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + + ), + ], + ), + ], + ), + + ], + ), + ), + ); + } + + Widget _buildForthRowLastColumn(bool isDesktop){ + return Container( + color: Colors.white, + child: Expanded( // Allow second column to take available space + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Third Approver", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: isDesktop, + width: isDesktop? MediaQuery.of(context).size.width * 0.2 : MediaQuery.of(context).size.width * 0.8, + child: SizedBox( + height: 40, + child: apiUserData == null + ? Center(child: Transform.scale(scale: 0.5, child: CircularProgressIndicator(),),) + : DropdownSearch( + selectedItem: userMap[selectedThirdApprover], + enabled: !isViewMode, + popupProps: PopupProps.menu( + showSearchBox: true, + fit: FlexFit.loose, // Allows flexible height + constraints: BoxConstraints(maxHeight: 250), + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Search User...", + contentPadding: EdgeInsets.symmetric(horizontal: 10), + ), + ), + ), + items: apiUserData!.map((user) { + return "${user['first_name']} ${user['last_name']}"; + }).toList(), + + dropdownDecoratorProps: DropDownDecoratorProps( + dropdownSearchDecoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 1,), + ), + + + ), + dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item + alignment: Alignment.centerLeft, + child: Text( + selectedItem ?? "Select", + style: TextStyle(fontSize: 12), + ), + ), + onChanged: (String? newValue) { + setState(() { + + selectedThirdApprover = userMap.entries + .firstWhere((entry) => entry.value == newValue) + .key; + + // if (selectedCountry!.isNotEmpty) { + // errorMessages.remove("country_code"); + // } + + }); + }, + ), + ), + ), + ], + ), + ], + ), + + ], + ), + ), + + ); + } + + +//----------------------------------- Submit --------------------------------------- + + List _buildSubmit(isDesktop){ + return[ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.blueAccent, width: 2), + ), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + onPressed: (){ + context.go('/listUser'); + }, + child: Text("Cancel") + + ), + SizedBox(width: 20,), + MouseRegion( + cursor: isViewMode ? SystemMouseCursors.forbidden : SystemMouseCursors.click, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: isViewMode ? Colors.blueAccent : Colors.blueAccent, // Keep original color + foregroundColor: isViewMode ? Colors.white : Colors.white, // Keep original color + disabledBackgroundColor: Colors.blueAccent, // Ensure color remains when disabled + disabledForegroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.blueAccent, width: 2), + ), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + onPressed: isViewMode ? null : handleSubmit, // Disable when in view mode + child: Text("Submit"), + ), + ) + + + ]; + } + + + + +} + +class DatabaseHelper { + // Example: Singleton pattern to ensure only one instance of the class exists + static final DatabaseHelper _instance = DatabaseHelper._internal(); + + // Private constructor to prevent external instantiation + DatabaseHelper._internal(); + + // Public method to access the instance + static DatabaseHelper get instance => _instance; + + // Add your database methods here + Future fetchData() async { + // Implement data fetching logic + } +} \ No newline at end of file diff --git a/lib/Screens/userManagement/user_List.dart b/lib/Screens/userManagement/user_List.dart new file mode 100644 index 0000000..de3cf83 --- /dev/null +++ b/lib/Screens/userManagement/user_List.dart @@ -0,0 +1,490 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:responsive_builder/responsive_builder.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../config/apiUrl.dart'; +import '../../routes/custom_appBar.dart'; +import '../../routes/custom_drawer.dart'; + +class UserListScreen extends StatefulWidget { + @override + _UserListScreenState createState() => _UserListScreenState(); +} + +class _UserListScreenState extends State { + late Future> futureUsers; + List? apiCountryData; + String? selectedUserId; + + Future getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('auth_token'); + + } + + Future> fetchUsers() async { + final String apiUrlData = '$apiUrl/api/users'; + final String? token = await getToken(); + + print("Fetch Users"); + print("TOEKRWE: $token"); + + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + final response = await http.get( + Uri.parse(apiUrlData), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['data']; // Returning raw JSON list + } else { + throw Exception('Failed to load users'); + } + } + + Future fetchCountryList() async { + final String apiUrldata = '$apiUrl/api/getcountryMaster'; + + final token = await getToken(); + + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + final response = await http.get( + Uri.parse(apiUrldata), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + try { + final data = json.decode(response.body); + print("Country - $data"); + + if (!data.containsKey('data') || data['data'] is!List) { + throw Exception("Invalid response format: 'data' field is missing or not a List"); + } + + List plansJson = data['data']; // 'data' is a Map, not a List + + if (data['data'] is List) { + List plansJson = data['data']; + print("plansJson.length - ${plansJson.length}"); + } else { + print("The 'data' key does not contain a list."); + } + + setState(() { + apiCountryData = plansJson; // Store API response in state + }); + print('plansJSONContry - $plansJson'); + + } catch (e) { + throw Exception('Error parsing response: $e'); + } + } else { + throw Exception('Failed to load plans'); + } + } + + + @override + void initState() { + super.initState(); + futureUsers = fetchUsers(); + fetchCountryList(); + } + + void handleDelete(userId){ + print("handDel - $userId"); + } + + + void handleToggleUserStatus(String userId, String currentStatus) async { + print("Toggling user status - $userId (Current: $currentStatus)"); + + final String apiUrlData = '$apiUrl/api/users/update/$userId'; // API for updating user + final String? token = await getToken(); + + if (token == null) { + print("Error: Token not found"); + return; + } + + // Toggle status: If active ("1"), set to inactive ("0"); otherwise, activate ("1") + String newStatus = (currentStatus == "1") ? "0" : "1"; + + try { + final response = await http.put( + Uri.parse(apiUrlData), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + "is_active": newStatus // Set new status dynamically + }), + ); + + if (response.statusCode == 200) { + print("User status updated successfully to $newStatus!"); + refreshUserList(); // Refresh users list after update + } else { + print("Failed to update user status. Status: ${response.statusCode}"); + print("Error: ${response.body}"); + } + } catch (e) { + print("Error updating user status: $e"); + } + } + +// Refresh user list after update + void refreshUserList() { + setState(() { + futureUsers = fetchUsers(); // Re-fetch users after status update + }); + } + + + @override + Widget build(BuildContext context) { + return ResponsiveBuilder(builder: (context, sizingInfo) { + bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop; + + + return Scaffold( + appBar: isDesktop ? null : const CustomAppBar(title:'User Management'), + drawer: isDesktop ? null : CustomDrawer(isDesktop: false), + body: + Container( + color: Colors.white, + child: Row( + children: [ + if(isDesktop) + CustomDrawer(isDesktop: true), + // const Expanded(child: Center(child: Text("User Page Content"))), + Expanded(child: buildUserTable(isDesktop)), + ], + ), + ), + ); + + + }); + } + + Widget buildUserTable(bool isDesktop) { + + + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Text('User List', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + IconButton( + icon: const Icon(Icons.keyboard_arrow_down), + onPressed: () { + + }, + ), + ], + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.blueAccent), + onPressed: () async { + + List users = await futureUsers; + + // Print the resolved value + print("CREATELIAS - $users"); + + context.go("/CreateUserDetails", + extra: { + // 'apiCountryData': apiCountryData, + 'apiUserData' :users, + } + ); + if (!isDesktop) Navigator.pop(context); + }, + child: Row( + children: [ + Icon(Icons.add_circle,color: Colors.white,), + SizedBox(width: 5,), + Text('New User'), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + + FutureBuilder>( + future: futureUsers, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text("Error: ${snapshot.error}")); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center(child: Text("No users found")); + } + + List users = snapshot.data!; + Color borderColor = Color(0xFF9E9DBD); + + return Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SizedBox( + width: MediaQuery.of(context).size.width * 1.5, + child: SingleChildScrollView( + + scrollDirection: Axis.horizontal, // Inner wrapper for vertical scrolling + + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), + // constraints: BoxConstraints(minWidth: 1300), + // width: MediaQuery.of(context).size.width , + + child: Container( + // color: Colors.amber, + child: DataTable( + columnSpacing: 20.0, // Adjust spacing between columns + dividerThickness: 0.5, + dataRowMinHeight: 60.0, // Minimum row height + dataRowMaxHeight: 100.0, + border: TableBorder( + horizontalInside: BorderSide(width: 0.5, color: Colors.grey.shade200), + ), + columns: const [ + + DataColumn(label: Text('User Details',style: TextStyle(color: Color(0xFF9E9DBD),fontSize: 15, fontWeight: FontWeight.bold),)), + DataColumn(label: Text('Role',style: TextStyle(color: Color(0xFF9E9DBD),fontSize: 15, fontWeight: FontWeight.bold),)), + DataColumn(label: Text('Level',style: TextStyle(color: Color(0xFF9E9DBD),fontSize: 15, fontWeight: FontWeight.bold),)), + DataColumn(label: Text('Status',style: TextStyle(color: Color(0xFF9E9DBD),fontSize: 15, fontWeight: FontWeight.bold),)), + DataColumn(label: Text('Actions',style: TextStyle(color: Color(0xFF9E9DBD),fontSize: 15, fontWeight: FontWeight.bold),)), + + ], + + rows: users.map((user) { + String userId = user['user_id'].toString(); // Get user ID + bool isSelected = selectedUserId == userId; + + return DataRow(cells: [ + // DataCell(Text(user['user_id'].toString())), + DataCell( + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Align(alignment: Alignment.center, + child: GestureDetector( + onTap: () { + setState(() { + selectedUserId = userId; // Store clicked user ID + }); + }, + child: Container( + width: 15, // Adjust size + height: 15, + decoration: BoxDecoration( + // Background color + shape: BoxShape.rectangle, + border: Border.all( + color: isSelected ? Colors.blueAccent : Color(0xFF9E9DBD), + // color: Color(0xFF9E9DBD), + // color: Color.fromRGBO(128, 128, 128, 0.6), + width: isSelected ?2:1), // Grey outline + ), + ), + ), + ), + + + + SizedBox(width: 50,), + Align( + alignment: Alignment.center, + child: Container( + + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Color(0xFF9E9DBD), width: 1), // Grey outline + ), + + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + + width: 40, // Adjust size + height: 40, + decoration: BoxDecoration( + color: Colors.amber, // Inner circle background + shape: BoxShape.circle, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + (user['first_name'] != null && user['first_name']!.isNotEmpty) + ? user['first_name']![0].toUpperCase() + : "?", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white, ), + ),], + + + ), + ), + )), + ), + SizedBox(width: 20,), + + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Text("${user['first_name'] ?? ''} ${user['last_name'] ?? ''}", + style: TextStyle( color: Colors.blueAccent,fontSize: 16),), + ],), + + SizedBox(height: 5), + + Row(children: [ + Icon(Icons.mail_outline, size: 15,color: Color(0xFF9E9EBE)), + SizedBox(width: 10,), + Text(user['email'] ?? '') + ],), + + SizedBox(height: 5), + Row(children: [ + Icon(Icons.account_tree_outlined, size: 15,color:Color(0xFF9E9EBE)), + SizedBox(width: 10,), + Text(user['user_type'] ?? '') ],), + ],), + + + ], + ) + ), + + DataCell(Text(user['role_id'] ?? 'N/A',style: TextStyle(color: user['is_active'] == "1" ? Colors.black :Colors.grey, fontWeight: FontWeight.bold),)), + DataCell(Text(user['level_id'] ?? 'N/A',style: TextStyle(color: user['is_active'] == "1" ? Colors.black :Colors.grey, fontWeight: FontWeight.bold),)), + DataCell( + GestureDetector( onTap :(){ + handleToggleUserStatus(user['user_id'], user['is_active']); + }, + child: Text( + user['is_active'] == "1" ? "Active" : "Inactive", + style: TextStyle(color: user['is_active'] == "1" ? Colors.lightGreen :Colors.grey , + + fontWeight: FontWeight.bold),) + + ) + + ), + + + DataCell( + Row( + children: [ + MouseRegion( + cursor: user['is_active'] == "0" ? SystemMouseCursors.forbidden : SystemMouseCursors.click, + child: IconButton( + icon: Icon(Icons.remove_red_eye, color: user['is_active'] == "0" ? Colors.grey : Colors.blueAccent), + onPressed: user['is_active'] == "0" + ? null + : () { + context.go( + "/CreateUserDetails", + extra: { + "selectedUser": user, + "isViewMode": true + + }, + ); + }, + ), + ), + MouseRegion( + cursor: user['is_active'] == "0" ? SystemMouseCursors.forbidden : SystemMouseCursors.click, + child: IconButton( + icon: Icon(Icons.edit, color: user['is_active'] == "0" ? Colors.grey : Colors.green), + onPressed: user['is_active'] == "0" + ? null + : () { + print("USER: $user"); + context.go( + "/CreateUserDetails", + extra: { + "selectedUser": user, + "isViewMode": false + + }, + ); + }, + ), + ), + MouseRegion( + cursor: user['is_active'] == "0" ? SystemMouseCursors.forbidden : SystemMouseCursors.click, + child: IconButton( + icon: Icon(Icons.delete, color: user['is_active'] == "0" ? Colors.grey : Colors.redAccent), + onPressed: user['is_active'] == "0" + ? null + : () { + print("USER ID: ${user['user_id']}"); + var userId = user['user_id']; + handleDelete(userId); + }, + ), + ), + ], + ), + ), + + ]); + }).toList(), + + ), + ), + ), + ), + ), + ), + ); + + }, + ), + + + + + ])); + } +} diff --git a/lib/routes/custom_drawer.dart b/lib/routes/custom_drawer.dart index 98fb8d4..474fc4f 100644 --- a/lib/routes/custom_drawer.dart +++ b/lib/routes/custom_drawer.dart @@ -1,37 +1,161 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:responsive_builder/responsive_builder.dart'; import 'package:shared_preferences/shared_preferences.dart'; -class CustomDrawer extends StatelessWidget{ - final bool isDesktop; + class CustomDrawer extends StatefulWidget{ + + final bool isDesktop; + const CustomDrawer({super.key, required this.isDesktop}); + + @override + _CustomDrawerState createState() => _CustomDrawerState(); + + } + +class _CustomDrawerState extends State{ + + String? token; + Map? userData; + Map? fetchedUserData; + Map userDetails = {}; + + @override + void initState() { + super.initState(); + initializeData(); + } + + Future initializeData() async{ + + print("initializeDatainitializeData"); + token = await getToken(); + fetchedUserData = await getUserData(); + + if(token == null || fetchedUserData == null) + { + print("Token or USerId missing"); + return; + } + + setState(() { + userData = fetchedUserData; + }); + + } + + + Future getToken() async{ + final prefs = await SharedPreferences.getInstance(); + return prefs.getString("auth_token"); + } + + Future ?> getUserData() async { + final prefs = await SharedPreferences.getInstance(); + final String? userDataString = prefs.getString('user_data'); + + if(userDataString != null){ + try{ + userDetails = jsonDecode(userDataString); + + return{ + "user_id" : userDetails["user_id"].toString(), + "name" : "${userDetails["first_name"]} ${userDetails["last_name"]}", + "email" : userDetails["email"] ?? "", + }; + }catch (e) { + print("Error decoding user data: $e"); + return null; + } + } + return null; + } + - const CustomDrawer({super.key, required this.isDesktop}); @override Widget build(BuildContext context){ - Widget drawerContent = Column( - children: [ - SizedBox( - height: 80, - child: const DrawerHeader( - decoration: BoxDecoration(color: Colors.blue), - child: SizedBox( - width: double.infinity, - child: Text('Menu', style: TextStyle(color: Colors.white, fontSize: 20))) + Widget drawerContent = Container( + color: Color(0xFFF3F3FA), + child: Column( + children: [ + GestureDetector( + onTap:(){ + print("ONTAP Custom"); + context.go( + "/CreateUserDetails", + extra: { + "selectedUser": userDetails, + "isEditProfile": true, + "isViewMode": true + }, + ); + }, + child :SizedBox( + height: 80, + child: Container( + color: Color(0xFFF3F3FA), + padding: EdgeInsets.all(16), + width: double.infinity, + child: Row( + + children: [ + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + height: 50, + width: 50, + decoration:BoxDecoration( + color: Colors.blueAccent, + shape: BoxShape.circle + ) , + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ Text( + userData?["name"]?.isNotEmpty == true + ? userData!["name"]![0].toUpperCase() + : "N/A", + style: TextStyle(color: Colors.white, fontSize: 25), + ),],), + ), + ), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + userData?["name"] ?? "N/A", + style: TextStyle(color: Colors.black87, fontSize: 11), + ), + Text( + userData?["email"] ?? "N/A", + style: TextStyle(color: Colors.black45, fontSize: 10), + ), + + ],) + ],) + ), ), - ), - _buildDrawerItem(context, Icons.home,'Home', '/home'), - _buildExpandableItem(context,Icons.assessment,'Plans',[ - _buildSubDrawerItem(context,'My Plans','/listPlan'), - // _buildSubDrawerItem(context,'PlanB','/PlanB') - ]), - _buildDrawerItem(context,Icons.logout,'Logout','/') - ], + ), + _buildDrawerItem(context, Icons.home,'Home', '/home'), + _buildExpandableItem(context,Icons.assessment,'Plans',[ + _buildSubDrawerItem(context,'My Travel Request','/listPlan'), + // _buildSubDrawerItem(context,'PlanB','/PlanB') + ]), + _buildExpandableItem(context,Icons.account_circle_outlined,'User ',[ + _buildSubDrawerItem(context,'User List','/listUser'), + // _buildSubDrawerItem(context,'PlanB','/PlanB') + ]), + _buildDrawerItem(context,Icons.logout,'Logout','/') + ], + ), ); - if (isDesktop) { + if (widget.isDesktop) { // Sidebar for Desktop (always visible)** return Container( width: 250, // Fixed width for sidebar @@ -74,14 +198,13 @@ class CustomDrawer extends StatelessWidget{ ); } - Widget _buildSubDrawerItem(BuildContext context, String title, String route) { return ListTile( title: Text(title), onTap: (){ context.go(route); - if (!isDesktop) Navigator.pop(context); + if (!widget.isDesktop) Navigator.pop(context); }, ); } diff --git a/lib/routes/custom_router.dart b/lib/routes/custom_router.dart index 0500a69..ca868b5 100644 --- a/lib/routes/custom_router.dart +++ b/lib/routes/custom_router.dart @@ -4,6 +4,8 @@ import 'package:frontend/Screens/authentication/loginPage1.dart'; import 'package:frontend/Screens/dashboard/home_page.dart'; import 'package:frontend/Screens/plans/create_plans.dart'; import 'package:frontend/Screens/plans/list_plans.dart'; +import 'package:frontend/Screens/userManagement/create_user/create_user.dart'; +import 'package:frontend/Screens/userManagement/user_List.dart'; import 'package:go_router/go_router.dart'; final GoRouter router = GoRouter( @@ -24,7 +26,13 @@ final GoRouter router = GoRouter( path: '/createPlan', builder: (context, state) => CreatePlan(), ), - - + GoRoute( + path: '/listUser', + builder: (context, state) => UserListScreen(), + ), + GoRoute( + path: '/CreateUserDetails', + builder: (context, state) => CreateUserForm(), + ), ], ); \ No newline at end of file diff --git a/lib/services/apiService.dart b/lib/services/apiService.dart new file mode 100644 index 0000000..5a383a9 --- /dev/null +++ b/lib/services/apiService.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'package:frontend/utils/auth_utils.dart'; +import 'package:http/http.dart' as http; +import '../../config/apiUrl.dart'; + + +class ApiService { + + Future> fetchCountryList() async { + + final String apiUrldata = '$apiUrl/api/getcountryMaster'; + final token = await getToken(); + + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + final response = await http.get( + Uri.parse(apiUrldata), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + try { + final data = json.decode(response.body); + print("Country - $data"); + + if (!data.containsKey('data') || data['data'] is! List) { + throw Exception("Invalid response format: 'data' field is missing or not a List"); + } + + return data['data']; + } catch (e) { + throw Exception('Error parsing response: $e'); + } + } else { + throw Exception('Failed to load country list'); + } + } + + Future> fetchUsers() async { + final String apiUrlData = '$apiUrl/api/users'; + final String? token = await getToken(); + + print("Fetch Users"); + print("TOEKRWE: $token"); + + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + final response = await http.get( + Uri.parse(apiUrlData), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return data['data']; // Returning raw JSON list + } else { + throw Exception('Failed to load users'); + } + } + + +} diff --git a/lib/utils/auth_utils.dart b/lib/utils/auth_utils.dart new file mode 100644 index 0000000..241da90 --- /dev/null +++ b/lib/utils/auth_utils.dart @@ -0,0 +1,6 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +Future getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString("auth_token"); +} diff --git a/lib/widgets/custom_text_forex.dart b/lib/widgets/custom_text_forex.dart index 001dce1..38bdecc 100644 --- a/lib/widgets/custom_text_forex.dart +++ b/lib/widgets/custom_text_forex.dart @@ -35,6 +35,7 @@ class _CustomTextFieldForexWrapperState extends State _CustomTextFieldUserWrapperState(); +} + +class _CustomTextFieldUserWrapperState extends State { + @override + Widget build(BuildContext context) { + return Container( + width: widget.width ?? // Use custom width if provided, else default + (widget.isDesktop + ? MediaQuery.of(context).size.width * 0.25 + : MediaQuery.of(context).size.width * 0.8), + padding: widget.padding, + decoration: BoxDecoration( + // color: widget.color, + color: Color(0xFFF7F7FB), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: widget.isFocused ? Color(0xFF78B4FC) : Color(0xFFD6D5E6), + width: widget.isFocused ? 2.0 : 0.5, + ), + boxShadow: widget.isFocused + ? [ + BoxShadow( + color: Color.fromRGBO(120, 180, 252, 0.3), + blurRadius: 10, + spreadRadius: 2, + offset: Offset(0, 4), + + ), + ] + : [], + ), + child: widget.child, + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..438e7e5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import file_picker import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index cc94b90..669a5a1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bcrypt: + dependency: "direct main" + description: + name: bcrypt + sha256: "9dc3f234d5935a76917a6056613e1a6d9b53f7fa56f98e24cd49b8969307764b" + url: "https://pub.dev" + source: hosted + version: "1.1.3" boolean_selector: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -105,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "36a1652d99cb6bf8ccc8b9f43aded1fd60b234d23ce78af422c07f950a436ef7" + url: "https://pub.dev" + source: hosted + version: "10.0.0" flutter: dependency: "direct main" description: flutter @@ -118,6 +142,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + url: "https://pub.dev" + source: hosted + version: "2.0.27" flutter_test: dependency: "direct dev" description: flutter @@ -453,6 +485,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + url: "https://pub.dev" + source: hosted + version: "5.10.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d4a2844..58fc8b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: easy_stepper: ^0.8.5+1 intl: ^0.20.2 dropdown_search: ^5.0.6 + file_picker: ^10.0.0 + bcrypt: ^1.1.3 dev_dependencies: From c8f2d798ea96cd30d4dd60b0ff5c9e96fbb91edc Mon Sep 17 00:00:00 2001 From: Venba Team Date: Thu, 3 Apr 2025 12:17:49 +0530 Subject: [PATCH 2/3] Policy UI --- lib/Screens/policy/policy.dart | 291 +++++++++++++++++++++++++ lib/Screens/policy/policyCriteria.dart | 0 lib/routes/custom_drawer.dart | 5 + lib/routes/custom_router.dart | 9 +- 4 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 lib/Screens/policy/policy.dart create mode 100644 lib/Screens/policy/policyCriteria.dart diff --git a/lib/Screens/policy/policy.dart b/lib/Screens/policy/policy.dart new file mode 100644 index 0000000..6e8c849 --- /dev/null +++ b/lib/Screens/policy/policy.dart @@ -0,0 +1,291 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:responsive_builder/responsive_builder.dart'; + +import '../../routes/custom_appBar.dart'; +import '../../routes/custom_drawer.dart'; + +class Policy extends StatefulWidget{ + + const Policy ({super.key}); + + @override + _PolicyState createState() => _PolicyState(); +} + +class _PolicyState extends State{ + + late String policyType = "domestic"; + int? selectedServiceIndex = 1; + late String selectedService; + + @override + Widget build(BuildContext context) { + return ResponsiveBuilder(builder: (context, sizingInfo) { + bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop; + + return Scaffold( + backgroundColor: Colors.white, + appBar: isDesktop ? null : const CustomAppBar(title: 'Home'), + drawer: isDesktop ? null : CustomDrawer(isDesktop: false), + body: Row( + children: [ + if (isDesktop) CustomDrawer(isDesktop: true), + Expanded(child: buildPolicyLayout(isDesktop)) + ], + ), + ); + }); + } + + Widget buildPolicyLayout(bool isDesktop){ + return Container( + margin: EdgeInsets.all(20.0), + height: MediaQuery.of(context).size.height, + // color: Colors.grey, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + // color: Color(0xFFF7F7FB), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + + Row( + children: [ + Expanded( + + child: Container( + // color: Colors.amber, + // color: Color(0xFFF7F7FB), + padding: EdgeInsets.all(5), + alignment: Alignment.center, + child: Text( + "Choose Policy Type", + style: TextStyle(fontSize: 20,), + ), + ), + + ), + ], + ), + + SizedBox(height: 10,), + + Row ( + children: [ + Expanded( + + child: GestureDetector( + onTap: (){ setState(() { + policyType = "domestic"; + });}, + child: Container( + padding: EdgeInsets.all(10), + color: policyType == "domestic" ? Colors.blueAccent.shade100 : Color(0xFFEBEBF7), + // color: Colors.blue.shade300, + child: Column( + children: [ + Text( + "Domestic", + style: TextStyle(color: policyType == "domestic" ? Colors.white :Colors.black87, fontSize: 18,fontWeight: FontWeight.bold), + ), + ], + ), + + ), + ), + ), + Expanded( + + child: GestureDetector( + onTap: (){ setState(() { + policyType = "international"; + });}, + + child: Container( + padding: EdgeInsets.all(10), + // color: Color(0xFFEBEBF7), + color: policyType == "international" ? Colors.blueAccent.shade100 : Color(0xFFEBEBF7), + // color: Color(0xFFE3F2FD), + + child: Column( + children: [ + Text( + "International", + style: TextStyle(color: policyType == "international" ? Colors.white :Colors.black87, fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + ), + ) + + ), + + ], + ), + + ], + ), + ), + + isDesktop ? + Expanded( + child: Row( + children: [ + + _buildPolicyCategoryList(isDesktop), + _buildPolicyCategory(isDesktop), + ], + ), + ) + + : Expanded( + child: Column( + children: [ + _buildPolicyCategoryList(isDesktop), + _buildPolicyCategory(isDesktop), + ], + ), + ) + + ], + ), + ); + } + + + Widget _buildPolicyCategoryList(bool isDesktop){ + return Container( + color: Colors.white, + // color: Colors.blueGrey.shade200, + width: isDesktop ? MediaQuery.of(context).size.width * 0.15 : null, + child: isDesktop ? Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPolicySubCategoryList(isDesktop) + + ], + ) + : Container( + // color: Colors.amber, + child: Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPolicySubCategoryList(isDesktop) + ], + ), + ), + ), + + ); + } + + Widget _buildPolicySubCategoryList(bool isDesktop) { + List services = [ + "Flight", "Train", "Bus", "Taxi", "Forex","Accommodation", + "Insurance", "Visa", "Miscellaneous" + ]; + + return Expanded( + child: SingleChildScrollView( + scrollDirection: isDesktop ? Axis.vertical : Axis.horizontal, + child: Flex( + direction: isDesktop ? Axis.vertical : Axis.horizontal, + + children: services.asMap().entries.map((entry) { + + int index = entry.key; + String service = entry.value; + bool isSelected = selectedServiceIndex == index; + + return SizedBox( + width: 180, + height: isDesktop? 65 : 50, + child:GestureDetector( + onTap :(){ + print("Selected Services - $service - $index"); + setState(() { + selectedServiceIndex = index; + selectedService = service; + }); + }, + + child: Container( + margin: EdgeInsets.all(8), + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + // color: Colors.blue, + color: isSelected ? Colors.blueAccent.shade100 : Color(0xFFEBEBF7), + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + service, + style: TextStyle(color: isSelected ? Colors.white : Colors.black87, + fontSize: 13, + fontWeight: isSelected ? FontWeight.bold :FontWeight.w100), + ), + ), + ) + ); + }).toList(), + + ), + ), + ); + } + + + Widget _buildPolicyCategory(bool isDesktop){ + return Expanded( + child: Container( + color: Colors.brown.shade100, + // color: Colors.white60, + + + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + + Row( + children: [ + Expanded( + child: Container( + child: Text( + "policy Data 1" + ), + ), + ), + ], + ), + + + Expanded( + child: Container( + color: Colors.brown.shade200, + child: Row( + children: [ + Expanded( + child: Container( + child: Text( + "policy Data 2" + ), + ), + ), + ], + ), + ), + ), + ], + ), + + + ), + ); + } + + +} \ No newline at end of file diff --git a/lib/Screens/policy/policyCriteria.dart b/lib/Screens/policy/policyCriteria.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/routes/custom_drawer.dart b/lib/routes/custom_drawer.dart index 474fc4f..fa5487f 100644 --- a/lib/routes/custom_drawer.dart +++ b/lib/routes/custom_drawer.dart @@ -150,6 +150,11 @@ class _CustomDrawerState extends State{ _buildSubDrawerItem(context,'User List','/listUser'), // _buildSubDrawerItem(context,'PlanB','/PlanB') ]), + + _buildExpandableItem(context,Icons.policy,'Policy ',[ + _buildSubDrawerItem(context,'Policy','/Policy'), + // _buildSubDrawerItem(context,'PlanB','/PlanB') + ]), _buildDrawerItem(context,Icons.logout,'Logout','/') ], ), diff --git a/lib/routes/custom_router.dart b/lib/routes/custom_router.dart index ca868b5..4b27bc1 100644 --- a/lib/routes/custom_router.dart +++ b/lib/routes/custom_router.dart @@ -4,6 +4,7 @@ import 'package:frontend/Screens/authentication/loginPage1.dart'; import 'package:frontend/Screens/dashboard/home_page.dart'; import 'package:frontend/Screens/plans/create_plans.dart'; import 'package:frontend/Screens/plans/list_plans.dart'; +import 'package:frontend/Screens/policy/policy.dart'; import 'package:frontend/Screens/userManagement/create_user/create_user.dart'; import 'package:frontend/Screens/userManagement/user_List.dart'; import 'package:go_router/go_router.dart'; @@ -22,7 +23,8 @@ final GoRouter router = GoRouter( GoRoute( path: '/listPlan', builder: (context, state) => ListPlans(), - ), GoRoute( + ), + GoRoute( path: '/createPlan', builder: (context, state) => CreatePlan(), ), @@ -34,5 +36,10 @@ final GoRouter router = GoRouter( path: '/CreateUserDetails', builder: (context, state) => CreateUserForm(), ), + GoRoute( + path: '/Policy', + builder: (context,state) => Policy(), + ), + ], ); \ No newline at end of file From 41308b2590b6209d31a47395a68df3d667d65d33 Mon Sep 17 00:00:00 2001 From: Venba Team Date: Fri, 4 Apr 2025 09:03:37 +0530 Subject: [PATCH 3/3] user management functionalities --- lib/Screens/plans/create_plans.dart | 3 +- lib/Screens/policy/policy.dart | 45 +- lib/Screens/policy/policyCriteria.dart | 157 +++++++ .../create_user/create_user.dart | 383 ++++++++++++------ lib/Screens/userManagement/user_List.dart | 2 - lib/services/apiService.dart | 91 +++++ lib/utils/auth_utils.dart | 6 + 7 files changed, 523 insertions(+), 164 deletions(-) diff --git a/lib/Screens/plans/create_plans.dart b/lib/Screens/plans/create_plans.dart index 80bb76b..8488744 100644 --- a/lib/Screens/plans/create_plans.dart +++ b/lib/Screens/plans/create_plans.dart @@ -335,7 +335,6 @@ class _CreateNewPlansState extends State { } - void getSelectedPlanFor(){ if (!mounted) return; @@ -590,7 +589,7 @@ class _CreateNewPlansState extends State { }); } - Future postPlanData(Map planData) async { + Future postPlanData(Map planData) async { final String apiUrldata = '$apiUrl/api/plans/createOrEditPlan'; final token = await getToken(); // Fetch token diff --git a/lib/Screens/policy/policy.dart b/lib/Screens/policy/policy.dart index 6e8c849..c2f8c40 100644 --- a/lib/Screens/policy/policy.dart +++ b/lib/Screens/policy/policy.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:frontend/Screens/policy/policyCriteria.dart'; import 'package:responsive_builder/responsive_builder.dart'; import '../../routes/custom_appBar.dart'; @@ -203,7 +204,8 @@ class _PolicyState extends State{ return SizedBox( width: 180, - height: isDesktop? 65 : 50, + height: isDesktop ? MediaQuery.of(context).size.height * 0.1 : 50, + child:GestureDetector( onTap :(){ print("Selected Services - $service - $index"); @@ -242,45 +244,10 @@ class _PolicyState extends State{ Widget _buildPolicyCategory(bool isDesktop){ return Expanded( child: Container( - color: Colors.brown.shade100, - // color: Colors.white60, + // color: Colors.brown.shade100, + color: Colors.white60, - - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - - Row( - children: [ - Expanded( - child: Container( - child: Text( - "policy Data 1" - ), - ), - ), - ], - ), - - - Expanded( - child: Container( - color: Colors.brown.shade200, - child: Row( - children: [ - Expanded( - child: Container( - child: Text( - "policy Data 2" - ), - ), - ), - ], - ), - ), - ), - ], - ), + child: PolicyCriteria(isDesktop: isDesktop) ), diff --git a/lib/Screens/policy/policyCriteria.dart b/lib/Screens/policy/policyCriteria.dart index e69de29..dfa6ae8 100644 --- a/lib/Screens/policy/policyCriteria.dart +++ b/lib/Screens/policy/policyCriteria.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; + +import '../../widgets/custom_text_field.dart'; +import '../../widgets/custom_user_form.dart'; + +class PolicyCriteria extends StatefulWidget{ + + bool isDesktop; + + PolicyCriteria({super.key, required this.isDesktop}); + @override + _PolicyCriteriaState createState() => _PolicyCriteriaState(); + +} + +class _PolicyCriteriaState extends State{ + + Widget build(BuildContext context){ + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + + SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text("Selected Policy Criteria For Flight", + style: TextStyle( + fontSize: 16, + color:Colors.grey, + fontWeight: FontWeight.bold + ) + ,) + ],), + + + SizedBox(height: 15), + Row( + children: [ + Expanded( + child: Container( + // color:Colors.grey, + padding: const EdgeInsets.only(top:5,bottom: 5,left:5, right: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Class", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: widget.isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + // controller: controllers["Fname"], + // enabled: !isViewMode, + onChanged: (value) { }, + decoration: InputDecoration( + labelText: "Class", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + + ], + ), + ], + ), + + SizedBox(height: 10), + + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Cost", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + SizedBox(height: 5), + CustomTextFieldUserWrapper( + isFocused: false, + isDesktop: widget.isDesktop, + child: SizedBox( + height: 40, + child: TextField( + style: TextStyle(fontSize: 12), + // controller: controllers["Fname"], + // enabled: !isViewMode, + onChanged: (value) { }, + decoration: InputDecoration( + labelText: "Cost", + labelStyle: TextStyle(fontSize: 12, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + ), + ), + ), + + ], + ), + ], + ), + + + ], + ), + + + ), + ), + ], + ), + SizedBox(height: 15), + + Expanded( + child: Container( + + decoration: BoxDecoration( + border: Border.all( + color: Color(0xFFEBEBF7), + + ), + borderRadius: BorderRadius.circular(8), + // color: Colors.brown.shade200, + ), + + child: Row( + children: [ + Expanded( + child: Container( + child: Text( + "policy Data 2" + ), + ), + ), + ], + ), + ), + ), + + ], + ); + } + +} \ No newline at end of file diff --git a/lib/Screens/userManagement/create_user/create_user.dart b/lib/Screens/userManagement/create_user/create_user.dart index 54e1384..8956816 100644 --- a/lib/Screens/userManagement/create_user/create_user.dart +++ b/lib/Screens/userManagement/create_user/create_user.dart @@ -44,8 +44,8 @@ class _CreateUserFormState extends State{ // late final List? apiCountryData ; late List? apiCountryData; - - + late List? apiCostData; + late List? apiRoleData; late List? apiUserData; Map? apiselectedUser; @@ -102,20 +102,20 @@ class _CreateUserFormState extends State{ "user_type" : selectedUserType, "role_id" : selectedRole, "department_id": selectedDepartment, - "level_id" : selectedLevel, + "group_id" : selectedLevel, "first_approver": selectedFirstApprover, "second_approver": selectedSecondApprover, "third_approver": selectedThirdApprover, "passport_number": controllers["passportNumber"]?.text, "place_of_issue": controllers["placeOfIssue"]?.text, - "passport_document": selectedFileNames, + "passport_document": base64PDF, "date_of_issue": controllers["dateOfIssue"]?.text, "date_of_expiry": controllers["dateOfExpiry"]?.text, "created_by": userId, "is_active": "1", - "passport_fileData": base64PDF, + // "passport_fileData": base64PDF, }; return data; } @@ -145,10 +145,10 @@ class _CreateUserFormState extends State{ controllers["dateOfExpiry"]?.text = apiselectedUser?["date_of_expiry"] ?? ""; - selectedCountry = apiselectedUser?["country"]?.toString() ?? ""; + selectedCountry = apiselectedUser?["country_code"]?.toString() ?? ""; selectedGender = apiselectedUser?["gender"]?.toString().trim() ?? ""; - selectedFileNames = apiselectedUser?["passport_document"]?.toString().trim() ?? ""; + base64PDF = apiselectedUser?["passport_document"]?.toString().trim() ?? ""; selectedUserType = apiselectedUser?["user_type"]?.toString().trim() ?? ""; selectedRole = apiselectedUser?["role_id"]?.toString().trim() ?? ""; selectedDepartment = apiselectedUser?["department_id"]?.toString().trim() ?? ""; @@ -178,6 +178,8 @@ class _CreateUserFormState extends State{ apiCountryData = null; apiUserData = null; apiselectedUser = null; + apiCostData = null; + apiRoleData = null; // Delay accessing context until the widget is fully initialized // WidgetsBinding.instance.addPostFrameCallback((_) { @@ -246,7 +248,9 @@ class _CreateUserFormState extends State{ initializeData(); fetchCountries(); + fetchDepartment(); fetchUsers(); + fetchRoles(); } @@ -261,11 +265,27 @@ class _CreateUserFormState extends State{ } } + + Future fetchDepartment() async { + try { + List department = await apiService.fetchCostCenter(); + setState(() { + apiCostData = department; + }); + } catch (e) { + print('Error fetching department list: $e'); + } + } + Future fetchUsers() async { try { List users = await apiService.fetchUsers(); setState(() { - apiUserData = users; + // apiUserData = users; + + apiUserData = users.where((user) => user["role_id"] == "3").toList(); + + print("APIUSerDATa - $apiUserData"); userList = apiUserData ?? []; userMap = { @@ -280,6 +300,24 @@ class _CreateUserFormState extends State{ } + Future fetchRoles() async { + try { + final response = await apiService.fetchMasterDropdown(); + + if (response is Map && response.containsKey("role")) { + List roleList = response["role"]; // Extract the list + setState(() { + apiRoleData = roleList; + }); + print("APIROLEData - $apiRoleData"); + } else { + print("Error: 'role' key not found or response is not a Map."); + } + } catch (e) { + print('Error fetching role list: $e'); + } + } + void printFormData() { for (var field in dataHeader) { @@ -346,6 +384,7 @@ class _CreateUserFormState extends State{ Map data = userDetials; if (!isValidData(data)) { + print("USERDETAILS : $userDetials"); print("Validation Failed: Required fields are missing."); setState(() {}); return; // Stop execution if validation fails @@ -362,7 +401,11 @@ class _CreateUserFormState extends State{ errorMessages.clear(); // Reset errors // Required fields that must not be empty - List requiredFields = ["first_name", "last_name","email","password","mobile_no"]; + List requiredFields = ["first_name", "last_name","email","mobile_no"]; + + if (apiselectedUser == null) { + requiredFields.add("password"); + } // Check validation for each field for (String field in requiredFields) { @@ -371,6 +414,7 @@ class _CreateUserFormState extends State{ } } + // Mobile number validation for both mobile_no and alternate_mobile_no if (data["mobile_no"] != null && data["mobile_no"].toString().isNotEmpty) { if (!RegExp(r"^\d{10}$").hasMatch(data["mobile_no"].toString())) { @@ -402,6 +446,8 @@ class _CreateUserFormState extends State{ } + + void pickPDFWeb() { html.FileUploadInputElement uploadInput = html.FileUploadInputElement(); uploadInput.accept = '.pdf'; @@ -416,35 +462,87 @@ class _CreateUserFormState extends State{ print('File picked: ${file.name}'); print('File size: ${file.size} bytes'); + // Ensure the file is a PDF + if (!file.type.contains("pdf")) { + print("Error: Not a PDF file"); + return; + } - // Update the state with the selected file name setState(() { - selectedFileNames = file.name; // Store only one file name + selectedFileNames = file.name; // Store file name passportDocumentBytes = reader.result as Uint8List; // Store file data - // 🔹 Convert file to Base64 - base64PDF = base64Encode(passportDocumentBytes! as List); - // File size check: Ensure the file size does not exceed 3MB - int maxFileSize = 3 * 1024 * 1024; // 3MB in bytes + // 🔹 Convert to Base64 properly + base64PDF = base64Encode(passportDocumentBytes!); - if (file.size > maxFileSize) { - print('Error: File size exceeds 3MB'); - // You can show an error message here if necessary - // For example: - // showError('File size cannot exceed 3MB'); + print('Base64 Length: ${base64PDF!.length}'); + print('Base64 (first 50 chars): ${base64PDF!.substring(0, 50)}'); + + // Ensure Base64 starts with "JVBERi0x" + if (!base64PDF!.startsWith("JVBERi0x")) { + print("Error: Base64 does not start with 'JVBERi0x'"); return; } - print('File size: ${file.size} bytes'); - print('Base64 Data: $base64PDF'); // Debugging + // 🔹 File size check: Ensure it does not exceed 3MB + int maxFileSize = 3 * 1024 * 1024; // 3MB in bytes + if (file.size > maxFileSize) { + print('Error: File size exceeds 3MB'); + return; + } }); - - - }); }); } + + // void pickPDFWeb() { + // html.FileUploadInputElement uploadInput = html.FileUploadInputElement(); + // uploadInput.accept = '.pdf'; + // uploadInput.click(); + // + // uploadInput.onChange.listen((e) { + // final file = uploadInput.files!.first; + // final reader = html.FileReader(); + // + // reader.readAsArrayBuffer(file); + // reader.onLoadEnd.listen((event) { + // print('File picked: ${file.name}'); + // print('File size: ${file.size} bytes'); + // + // + // // Update the state with the selected file name + // setState(() { + // selectedFileNames = file.name; // Store only one file name + // passportDocumentBytes = reader.result as Uint8List; // Store file data + // + // // 🔹 Convert file to Base64 + // base64PDF = base64Encode(passportDocumentBytes! as List); + // // File size check: Ensure the file size does not exceed 3MB + // int maxFileSize = 3 * 1024 * 1024; // 3MB in bytes + // + // if (file.size > maxFileSize) { + // print('Error: File size exceeds 3MB'); + // // You can show an error message here if necessary + // // For example: + // // showError('File size cannot exceed 3MB'); + // return; + // } + // + // print('File size: ${file.size} bytes'); + // print('Base64 Data: $base64PDF'); // Debugging + // }); + // + // + // + // }); + // }); + // } + + + + + Future createUserData(Map userData) async { bool isUpdating = apiselectedUser != null && apiselectedUser!.isNotEmpty; @@ -624,7 +722,6 @@ class _CreateUserFormState extends State{ Text("Personal Details", style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold, // color: Colors.black, color: Color(0xFF8B8FB2) - ), ) ],), @@ -1238,36 +1335,7 @@ class _CreateUserFormState extends State{ children: [ Text("Change Password", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), SizedBox(height: 5), - CustomTextFieldUserWrapper( - isFocused: false, - isDesktop: isDesktop, - child: SizedBox( - height: 40, - child: TextField( - style: TextStyle(fontSize: 12), - controller: controllers["password"], - enabled: !isViewMode, - onChanged: (value) { - _clearError("changePassword"); - handleChangePassword(value); - }, - decoration: InputDecoration( - labelText: "Change Password", - labelStyle: TextStyle(fontSize: 12, color: Colors.grey), - floatingLabelBehavior: FloatingLabelBehavior.never, - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 16), - ), - ), - ), - ), - if (errorMessages["password"] != null) ...[ - SizedBox(height: 5), // Space before error message - Text( - errorMessages["password"]!, - style: TextStyle(color: Colors.red, fontSize: 12), - ), - ], + ], ), ], @@ -1621,6 +1689,7 @@ class _CreateUserFormState extends State{ children: [ Text("Passport Document", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), SizedBox(height: 5), + CustomTextFieldUserWrapper( isFocused: false, isDesktop: isDesktop, @@ -1641,6 +1710,101 @@ class _CreateUserFormState extends State{ ), ), ), + SizedBox(height: 10), + if(base64PDF != null) + + // Centers the text + Container( + color: Colors.white, + width : isDesktop + ? MediaQuery.of(context).size.width * 0.25 + : MediaQuery.of(context).size.width * 0.8, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + GestureDetector(onTap: (){ + print('DOWLOAS- $base64PDF '); + + if (base64PDF != null && base64PDF!.isNotEmpty) { + try { + // ✅ Step 1: Clean the Base64 string + String cleanedBase64 = base64PDF! + .replaceAll("\n", "") // Remove newlines + .replaceAll("\r", "") // Remove carriage returns + .replaceAll(" ", "") // Remove spaces + .trim(); // Trim any whitespace + + // ✅ Step 2: Ensure valid Base64 length (multiple of 4) + while (cleanedBase64.length % 4 != 0) { + cleanedBase64 += "_"; // Add '=' padding + } + + // ✅ Step 3: Decode the cleaned Base64 + Uint8List bytes; + try { + bytes = base64Decode(cleanedBase64); + } catch (e) { + print("Base64 decoding failed: $e"); + return; + } + + // ✅ Step 4: Create a Blob for download + final blob = html.Blob([bytes], 'application/pdf'); + final url = html.Url.createObjectUrlFromBlob(blob); + + // ✅ Step 5: Trigger the file download + final anchor = html.AnchorElement(href: url) + ..setAttribute("download", selectedFileNames ?? "document.pdf") + ..style.display = "none"; + + html.document.body!.append(anchor); + anchor.click(); + + // ✅ Step 6: Clean up + anchor.remove(); + html.Url.revokeObjectUrl(url); + + print("Download successful!"); + } catch (e) { + print("Error downloading file: $e"); + } + } else { + print("No file to download."); + } + + } , + + child: Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Colors.green.shade300, + ), + + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "Download", + style: TextStyle(fontSize: 12, color: Colors.white,fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.end, // Ensures text is centered within the Text widget + ), + SizedBox(width: 5), + Icon(Icons.download ,size: 13, color: Colors.white,) + ], + ), + + + ), + ) + + ], + ), + ), + @@ -1933,7 +2097,7 @@ class _CreateUserFormState extends State{ constraints: BoxConstraints(maxHeight: 250), ), - items: ["Admin", "Manager", "HR"], + items: ["Normal", "Privilege",], dropdownDecoratorProps: DropDownDecoratorProps( dropdownSearchDecoration: InputDecoration( border: InputBorder.none, @@ -1952,6 +2116,8 @@ class _CreateUserFormState extends State{ // Find the country_code based on selected country_name selectedUserType = newValue; + // print("selectedUserType - $selectedUserType"); + // if (selectedCountry!.isNotEmpty) { // errorMessages.remove("country_code"); // } @@ -1989,43 +2155,32 @@ class _CreateUserFormState extends State{ Text("Role ", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), SizedBox(height: 5), CustomTextFieldUserWrapper( - isFocused: false, + isFocused: false, // Dropdown doesn't use focus isDesktop: isDesktop, child: SizedBox( - height: 40, - child: DropdownSearch( - selectedItem: selectedRole, - enabled: !isViewMode, - popupProps: PopupProps.menu( - // showSearchBox: true, - fit: FlexFit.loose, // Allows flexible height - constraints: BoxConstraints(maxHeight: 250), + height: 45, // Set appropriate height + child: DropdownButtonFormField( + value: selectedRole, + style: TextStyle(fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: + EdgeInsets.symmetric(horizontal: 10), // Proper padding ), - items: ["Role 1", "Role 2", "Role 3"], - dropdownDecoratorProps: DropDownDecoratorProps( - dropdownSearchDecoration: InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 1,), - ), - ), - dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item - alignment: Alignment.centerLeft, - child: Text( - selectedItem ?? "Select", - style: TextStyle(fontSize: 12), - ), - ), - onChanged: (String? newValue) { + onChanged: isViewMode ? null : (newValue) { setState(() { - // Find the country_code based on selected country_name - selectedRole = newValue; - - // if (selectedCountry!.isNotEmpty) { - // errorMessages.remove("country_code"); - // } - + selectedRole = newValue; }); }, + + items:apiRoleData?.map>((item){ + return DropdownMenuItem( + value: item['dropdown_key'], // ID as value + child:Text(item['dropdown_value'] ?? "Select") + + ); + }).toList(), + hint: Text("Select"), ), ), ), @@ -2039,7 +2194,7 @@ class _CreateUserFormState extends State{ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Level", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), + Text("Group", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), SizedBox(height: 5), CustomTextFieldUserWrapper( isFocused: false, @@ -2097,44 +2252,30 @@ class _CreateUserFormState extends State{ Text("Department", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)), SizedBox(height: 5), CustomTextFieldUserWrapper( - isFocused: false, + isFocused: false, // Dropdown doesn't use focus isDesktop: isDesktop, child: SizedBox( - height: 40, - child: DropdownSearch( - selectedItem: selectedDepartment, - enabled: !isViewMode, - popupProps: PopupProps.menu( - // showSearchBox: true, - fit: FlexFit.loose, // Allows flexible height - constraints: BoxConstraints(maxHeight: 250), - + height: 45, // Set appropriate height + child: DropdownButtonFormField( + value: selectedDepartment, + style: TextStyle(fontSize: 12), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: + EdgeInsets.symmetric(horizontal: 10), // Proper padding ), - items: ["Dep1", "Dep2", "Dep3"], - dropdownDecoratorProps: DropDownDecoratorProps( - dropdownSearchDecoration: InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 1,), - ), - ), - dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item - alignment: Alignment.centerLeft, - child: Text( - selectedItem ?? "Select", - style: TextStyle(fontSize: 12), - ), - ), - onChanged: (String? newValue) { + onChanged: isViewMode ? null : (newValue) { setState(() { - // Find the country_code based on selected country_name - selectedDepartment = newValue; - - // if (selectedCountry!.isNotEmpty) { - // errorMessages.remove("country_code"); - // } - + selectedDepartment = newValue; }); }, + items:apiCostData?.map>((item){ + return DropdownMenuItem( + value: item['department_id'], // ID as value + child: Text(item['name'] ?? "Unknown"), + ); + }).toList(), + hint: Text("Select"), ), ), ), diff --git a/lib/Screens/userManagement/user_List.dart b/lib/Screens/userManagement/user_List.dart index de3cf83..32e4e2d 100644 --- a/lib/Screens/userManagement/user_List.dart +++ b/lib/Screens/userManagement/user_List.dart @@ -388,8 +388,6 @@ class _UserListScreenState extends State { SizedBox(width: 10,), Text(user['user_type'] ?? '') ],), ],), - - ], ) ), diff --git a/lib/services/apiService.dart b/lib/services/apiService.dart index 5a383a9..91768b4 100644 --- a/lib/services/apiService.dart +++ b/lib/services/apiService.dart @@ -69,4 +69,95 @@ class ApiService { } + Future fetchCostCenter() async { + final String apiUrldata = '$apiUrl/api/getCostCenterMaster'; + + final token = await getToken(); + + final userId = await getUserId(); + + // print("SUSRTRT- $userId"); + // + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + final response = await http.get( + Uri.parse(apiUrldata), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + try { + final data = json.decode(response.body); + print(data); + + if (!data.containsKey('data') || data['data'] is!List) { + throw Exception("Invalid response format: 'data' field is missing or not a List"); + } + + List plansJson = data['data']; // 'data' is a Map, not a List + // setState(() { + // apiCostData = plansJson; // Store API response in state + // if(apiCostData!.isNotEmpty){ + // selectedCostCenterId =apiCostData?.first['department_id']; + // } + + // if (apiCostData != null && apiCostData!.isNotEmpty) { + // selectedCostCenterId ??= apiCostData!.first['department_id']?.toString(); + // } + // }); + + print('plansJSON'); + + return plansJson; + + + } catch (e) { + throw Exception('Error parsing response: $e'); + } + } else { + throw Exception('Failed to load plans'); + } + } + + Future> fetchMasterDropdown() async { + final String apiUrldata = '$apiUrl/api/getDropdownMaster'; + + final token = await getToken(); + + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + final response = await http.get( + Uri.parse(apiUrldata), + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json',},); + + if (response.statusCode == 200) { + try { + final data = json.decode(response.body); + print(data); + if (!data.containsKey('data') || data['data'] is! Map) { + throw Exception("Invalid response format: 'data' field is missing or not a Map"); + } + + Map plansJson = data['data']; // 'data' is a Map, not a List + + return plansJson; + + } catch (e) { + throw Exception('Error parsing response: $e'); + } + } else { + throw Exception('Failed to load plans'); + } + } + + } diff --git a/lib/utils/auth_utils.dart b/lib/utils/auth_utils.dart index 241da90..5e35a51 100644 --- a/lib/utils/auth_utils.dart +++ b/lib/utils/auth_utils.dart @@ -4,3 +4,9 @@ Future getToken() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString("auth_token"); } + + +Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('userId'); +} \ No newline at end of file