From 6c7c41008a96dea291b2854acf1bd5c910ee9371 Mon Sep 17 00:00:00 2001 From: VE10-Sanjeev Date: Mon, 26 May 2025 11:53:33 +0000 Subject: [PATCH] hotels --- lib/Screens/forex/forexDetails.dart | 2 +- lib/Screens/hotels/hotelsDetails.dart | 566 +++++++++++++++++ lib/Screens/hotels/hotels_list.dart | 851 ++++++++++++++++++++++++++ lib/routes/custom_router.dart | 5 + lib/routes/organizationSetting.dart | 7 + lib/services/apiService.dart | 45 ++ 6 files changed, 1475 insertions(+), 1 deletion(-) create mode 100644 lib/Screens/hotels/hotelsDetails.dart create mode 100644 lib/Screens/hotels/hotels_list.dart diff --git a/lib/Screens/forex/forexDetails.dart b/lib/Screens/forex/forexDetails.dart index 0291648..d3df6c5 100644 --- a/lib/Screens/forex/forexDetails.dart +++ b/lib/Screens/forex/forexDetails.dart @@ -296,7 +296,7 @@ class ForexDataState extends State { Row( children: [ Text( - 'Create Perdiem Amount', + (forexDataId != null) ? 'Edit Perdiem Amount' : 'Create Perdiem Amount', style: GoogleFonts.poppins(fontSize: 15, color: Colors.black), ), const Spacer(), diff --git a/lib/Screens/hotels/hotelsDetails.dart b/lib/Screens/hotels/hotelsDetails.dart new file mode 100644 index 0000000..9c3e4de --- /dev/null +++ b/lib/Screens/hotels/hotelsDetails.dart @@ -0,0 +1,566 @@ +import 'dart:convert'; + +import 'package:dropdown_search/dropdown_search.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; + +import '../../config/apiUrl.dart'; +import '../../services/apiService.dart'; +import '../../utils/auth_utils.dart'; +import '../../widgets/custom_text_forex.dart'; +import 'hotels_list.dart'; + +class HotelsData extends StatefulWidget { + final Future> Function() fetchGetHotels; + final bool isDesktop; + final Color? layoutColor; + + final int? hotelsId; // <-- Add this + final Map? hotelsData; + + const HotelsData( + {super.key, + required this.isDesktop, + this.layoutColor, + required this.fetchGetHotels, + this.hotelsId, + this.hotelsData}); + + @override + HotelsDataState createState() => HotelsDataState(); +} + +class HotelsDataState extends State { + final ApiService apiService = ApiService(); + Map countryMap = {}; + late List? apiCountryData; + late List? apiAirlineCountryData; + Map? apiData; + final Map controllers = {}; + Map errorMessages = {}; + + List countryList = []; + String? selectedCountry; + String? selectedCountryName; + String? selectedCity; + String? selectedDuration; + String? selectedPerdiemAmount; + String? userId; + int? hotelsDataId; + late String isActive = "1"; + + List dataHeader = [ + "hotel_name", + "hotel_chain", + "country_code", + "city", + ]; + + Map hotels_Details() { + final data = { + "hotel_name": controllers["hotel_name"]?.text, + "hotel_chain": controllers["hotel_chain"]?.text, + "country_code": selectedCountry, + "country_name": selectedCountryName, + "city": controllers["city"]?.text, + "created_by": userId, + "is_active": isActive, + }; + return data; + } + + @override + void initState() { + super.initState(); + + apiCountryData = null; + apiData = null; + for (var field in dataHeader) { + controllers[field] = TextEditingController(); + } + fetchCountries(); + if (widget.hotelsId != null) { + print('Editing Hotles ID: ${widget.hotelsId}'); + updateHotelsDetails(); + } + _clearError(); + } + + void _clearError() { + setState(() { + errorMessages.clear(); + }); + } + + @override + void dispose() { + for (var controller in controllers.values) { + controller.dispose(); + } + super.dispose(); + } + + void updateHotelsDetails() { + print("Update - ${widget.hotelsData}"); + + final data = widget.hotelsData; + + if (data == null) return; + setState(() { + selectedCountry = data['country_code']; // For dropdown + selectedCountryName = data['country_name']; // For dropdown label or display + controllers['city']?.text = data['city'] ?? ''; + controllers['hotel_chain']?.text = data['hotel_chain'] ?? ''; + controllers['hotel_name']?.text = data['hotel_name'] ?? ''; + isActive = data["is_active"]; + final hotelsId = int.tryParse(data['hotel_id'].toString()); + hotelsDataId = hotelsId; + }); + } + + Future fetchCountries() async { + try { + List countries = await apiService.fetchCountryList(); + setState(() { + apiCountryData = countries; + }); + } catch (e) { + print('Error fetching country list: $e'); + } + } + + void toggleStatus() { + setState(() { + isActive = isActive == "1" ? "0" : "1"; + }); + } + + bool validateData() { + errorMessages.clear(); + + final data = { + "hotel_name": controllers["hotel_name"]?.text, + "hotel_chain": controllers["hotel_chain"]?.text, + "country_code": selectedCountry, + "country": selectedCountryName, + "city": controllers["city"]?.text, + }; + + final requiredFields = ["hotel_name","hotel_chain","country_code","city"]; + + // Check validation for each field + for (String field in requiredFields) { + if (data[field] == null || data[field].toString().trim().isEmpty) { + errorMessages[field] = "Required"; + } + } + return errorMessages.isEmpty; + } + + Future handleSubmit() async { + userId = await getUserId(); + + setState(() { + // This triggers UI rebuild with error messages + if (validateData()) { + postHotelsData(); + } + }); + + final hotelsData1 = hotels_Details(); + print("submit data - $hotelsData1"); + } + + Future postHotelsData({int isActive = 1}) async { + + final hotelsData = hotels_Details(); + + final String apiUrldata; + if (hotelsDataId != null) { + print("for edit hotel id - $hotelsDataId"); + + apiUrldata = '$apiUrl/api/updateHotels/$hotelsDataId'; + hotelsData["hotel_id"] = hotelsDataId.toString(); + hotelsData["updated_by"] = userId; + (hotelsData.containsKey("created_by")) ? hotelsData.remove("created_by") : '' ; + (hotelsData.containsKey("country_name")) ? hotelsData.remove("country_name") : '' ; + + + } else { + print("for add Hotel id - null"); + apiUrldata = '$apiUrl/api/createHotels'; + print("called apiUrl - $apiUrldata"); + hotelsData["created_by"] = userId; + (hotelsData.containsKey("country_name")) ? hotelsData.remove("country_name") : '' ; + } + + final token = await getToken(); // Fetch token + + if (token == null) { + throw Exception('Token not found. Please log in.'); + } + + try { + final uri = Uri.parse(apiUrldata); + final headers = { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }; + final body = jsonEncode(hotelsData); + + final response = hotelsDataId != null + ? await http.put(uri, headers: headers, body: body) + : await http.post(uri, headers: headers, body: body); + + if (response.statusCode == 200 || response.statusCode == 201) { + print("Hotels Details Created successfully!"); + print("Response: ${response.body}"); + // _clearError(); + _clearError(); + await widget.fetchGetHotels(); + + // dispose(); + Navigator.of(context).pop(); + } else if (response.statusCode == 404) { + Navigator.of(context).pop(); + final message = jsonDecode(response.body)['message'] ?? 'Unknown error'; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.redAccent, + behavior: SnackBarBehavior.floating, + ), + ); + } else { + print("Failed to submit plan. Status: ${response.statusCode}"); + print("Error: ${response.body}"); + } + } catch (e) { + print(" Error submitting plan: $e"); + } + } + + @override + Widget build(BuildContext context) { + late Map countryMap; // Mapping country_code -> country_name + late List countryCodes; // List of country codes + + // countryList = []; + 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 AlertDialog( + backgroundColor: Colors.white, + contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30), + // contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Row 1: Title + Edit + Delete buttons + Row( + children: [ + Text( + (hotelsDataId != null) ? 'Edit Hotels' : 'Create Hotels', + style: GoogleFonts.poppins(fontSize: 15, color: Colors.black), + ), + const Spacer(), + ], + ), + const SizedBox(height: 2), + Divider( + thickness: 0.2, + color: Colors.blueGrey.shade100, + ), + const SizedBox(height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Hotel Name", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF575A74)), + ), + SizedBox(height: 5), + CustomTextFieldForexWrapper( + isFocused: false, + isDesktop: widget.isDesktop, + color: Colors.transparent, + child: SizedBox( + height: 40, + child: TextField( + controller: controllers["hotel_name"], + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + labelText: "Hotel Name", + labelStyle: TextStyle(fontSize: 11, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + )), + ), + if (errorMessages["hotel_name"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["hotel_name"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + const SizedBox(height: 5), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Hotel Chain", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF575A74)), + ), + SizedBox(height: 5), + CustomTextFieldForexWrapper( + isFocused: false, + isDesktop: widget.isDesktop, + color: Colors.transparent, + child: SizedBox( + height: 40, + child: TextField( + controller: controllers["hotel_chain"], + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + labelText: "Hotel Chain", + labelStyle: TextStyle(fontSize: 11, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + )), + ), + if (errorMessages["hotel_chain"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["hotel_chain"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + SizedBox(height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "City", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF575A74)), + ), + SizedBox(height: 5), + CustomTextFieldForexWrapper( + isFocused: false, + isDesktop: widget.isDesktop, + color: Colors.transparent, + child: SizedBox( + height: 40, + child: TextField( + controller: controllers["city"], + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + labelText: "City", + labelStyle: TextStyle(fontSize: 11, color: Colors.grey), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16), + ), + )), + ), + if (errorMessages["city"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["city"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + const SizedBox(height: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Country", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF575A74)), + ), + SizedBox(height: 5), + CustomTextFieldForexWrapper( + isFocused: false, + isDesktop: widget.isDesktop, + child: SizedBox( + height: 40, + child: DropdownSearch( + selectedItem: countryMap[selectedCountry], + popupProps: PopupProps.menu( + showSearchBox: true, // Enables search functionality + menuProps: const MenuProps( + backgroundColor: Colors.white, + ), + constraints: BoxConstraints(maxHeight: 250), + itemBuilder: (context, item, isSelected) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 6.0), + child: Text( + item, + style: GoogleFonts.poppins(fontSize: 11.5), + ), + ), + searchFieldProps: TextFieldProps( + decoration: InputDecoration( + hintText: "Search Country...", + hintStyle: GoogleFonts.poppins(fontSize: 11), + contentPadding: EdgeInsets.symmetric(horizontal: 4), + ), + ), + ), + 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: GoogleFonts.poppins(fontSize: 11), + ), + ), + onChanged: (String? newValue) { + setState(() { + // Find the country_code based on selected country_name + selectedCountry = countryMap.entries + .firstWhere((entry) => entry.value == newValue) + .key; + selectedCountryName = newValue; + }); + }, + ), + ), + ), + if (errorMessages["country_code"] != null) ...[ + SizedBox(height: 5), // Space before error message + Text( + errorMessages["country_code"]!, + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ], + ], + ), + SizedBox( height: 15 ), + // if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,), + if (hotelsDataId != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Change Status ", + style: GoogleFonts.poppins( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF575A74)), + ), + Tooltip( + message: + isActive == "1" ? "Tap to deactivate" : "Tap to activate", + child: GestureDetector( + onTap: toggleStatus, + child: Text( + isActive == "1" ? "Active" : "Inactive", + style: TextStyle( + fontSize: 13, + fontFamily: "Inter", + color: isActive == "1" ? Colors.green : Colors.red, + ), + ), + ), + ) + ], + ), + if (hotelsDataId != null) + SizedBox( + height: 15, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // SizedBox( + // child: ElevatedButton( + // onPressed: () { + // // You can get text from commentController.text + // Navigator.of(context).pop(); // Close the modal + // }, + // style: ElevatedButton.styleFrom( + // backgroundColor: widget.layoutColor, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8), + // ), + // ), + // child: Text('Cancel', + // style: GoogleFonts.poppins( + // fontSize: 13, color: Colors.white)), + // ), + // ), + // SizedBox( + // width: 10, + // ), + SizedBox( + child: ElevatedButton( + onPressed: () { + handleSubmit(); + // You can get text from commentController.text + // Navigator.of(context).pop(); // Close the modal + }, + style: ElevatedButton.styleFrom( + backgroundColor: widget.layoutColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text('Save', + style: GoogleFonts.poppins( + fontSize: 11, color: Colors.white)), + ), + ), + ], + ) + // : SizedBox.shrink(), + ], + ), + ); + } +} diff --git a/lib/Screens/hotels/hotels_list.dart b/lib/Screens/hotels/hotels_list.dart new file mode 100644 index 0000000..281eba6 --- /dev/null +++ b/lib/Screens/hotels/hotels_list.dart @@ -0,0 +1,851 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.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'; +import '../../services/apiService.dart'; +import '../../utils/auth_utils.dart'; +import '../../utils/pagination.dart'; +import '../../widgets/popup_userList_action.dart'; +import 'hotelsDetails.dart'; + +class HotelsDataList extends StatefulWidget { + const HotelsDataList({super.key}); + + @override + HotelsDataListState createState() => HotelsDataListState(); +} + +class HotelsDataListState extends State { + final GlobalKey hotelsListKey = + GlobalKey(); + + final ApiService apiService = ApiService(); + late Future> futureHotels; + + late Map userSingleData; + List? apiCountryData; + String? selectedUserId; + String? orgId; + + Color? layoutColor; + Color? bodyColor; + + List allHotels = []; + List filteredHotels = []; + TextEditingController searchController = TextEditingController(); + + int currentPage = 0; + int itemsPerPage = 10; + + @override + void initState() { + super.initState(); + futureHotels = fetchGetHotels(); + + futureHotels.then((objects) { + setState(() { + allHotels = objects; + }); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + fetchCountryList(); + loadInitialData(); + }); + + // futurePlans = fetchPlans(); + } + + Future> refreshData() { + print("Calling Refresh Data"); + + futureHotels = fetchGetHotels(); + + return futureHotels.then((objects) { + setState(() { + allHotels = objects; + }); + return objects; + }); + } + + void loadInitialData() async { + String? layoutString = await getLayoutColor(); + String? bodyStringColor = await getBodyColor(); + + setState(() { + layoutColor = layoutString != null + ? Color(int.parse(layoutString)) + : Colors.redAccent; + + bodyColor = bodyStringColor != null + ? Color(int.parse(bodyStringColor)) + : Colors.white; + }); + } + + Future getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('auth_token'); + } + + Future> fetchGetHotels() async { + orgId = await getOrgId(); + final String apiUrlData = '$apiUrl/api/getHotels'; + + final String? token = await getToken(); + + print("Fetch Hotels 2KN : $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 Master - $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'); + } + } + + +// Refresh user list after update + void refreshUserList() { + setState(() { + futureHotels = fetchGetHotels(); // Re-fetch users after status update + // Wait for futurePlans to be fetched and update allPlans + }); + } + + void filterHotels(String query) { + print("allHotels before filtering: $query"); + final lowerQuery = query.toLowerCase(); + setState(() { + filteredHotels = allHotels.where((hotels) { + final isActiveStatus = + hotels['is_active'] == "1" ? "active" : "inactive"; + return (hotels['country_code']?.toLowerCase().contains(lowerQuery) ?? + false) || + (hotels['country_name']?.toLowerCase().contains(lowerQuery) ?? + false) || + (hotels['city']?.toLowerCase().contains(lowerQuery) ?? false) || + (hotels['hotel_chain']?.toLowerCase().contains(lowerQuery) ?? false) || + (hotels['hotel_name']?.toLowerCase().contains(lowerQuery) ?? + false) || + (isActiveStatus.contains(lowerQuery)); + }).toList(); + }); + print("filteredHotels: $filteredHotels"); + } + + @override + Widget build(BuildContext context) { + return ResponsiveBuilder(builder: (context, sizingInfo) { + bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop; + + return Scaffold( + backgroundColor: Color(0xFFf5f5f5), + // appBar: isDesktop ? null : const CustomAppBar(title: 'User Management'), + // drawer: isDesktop ? null : CustomDrawer(isDesktop: false), + appBar: CustomAppBar(isDesktop: isDesktop), + drawer: CustomDrawer(isDesktop: false), + body: Padding( + padding: isDesktop + ? EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * + 0.1, // 30% of screen width as horizontal padding + vertical: MediaQuery.of(context).size.height * + 0, // 5% of screen height as vertical padding + ) + : EdgeInsets.all(0), + child: Row( + children: [ + // if (isDesktop) CustomDrawer(isDesktop: true), + // const Expanded(child: Center(child: Text("User Page Content"))), + Expanded(child: buildGroupList(isDesktop)), + ], + ), + ), + ); + }); + } + + Widget buildGroupList(bool isDesktop) { + return Container( + margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null, + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: isDesktop ? Colors.white : Color(0xFFFCFCFC), + ), + // decoration: BoxDecoration( + // // color: Colors.amber, + // // color: bodyColor, + // color: Color(0xFFE1F5FE), + // border: Border.all( + // color: Colors.white, + // // color: Color(0xFFF7F7FB), + // width: 3.5)), + child: buildUserTable(isDesktop), + ); + } + + Widget buildUserTable(bool isDesktop) { + return Container( + // margin: isDesktop + // ? EdgeInsets.all(10.0) + // : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0), + // padding: const EdgeInsets.all(10), + height: isDesktop + ? MediaQuery.of(context).size.height * 0.98 + : MediaQuery.of(context).size.height, + + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Container( + color: Colors.white, + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Divider( + // thickness: 0.2, // how "thick" the line is + // color: Colors.grey, // optional + // ), + Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + 'Hotel Details', + style: GoogleFonts.poppins( + fontSize: isDesktop ? 16 : 14, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ], + ), + if (isDesktop) + SizedBox( + width: MediaQuery.of(context).size.width * 0.16, + ), + + if (isDesktop) + Container( + width: MediaQuery.of(context).size.width * 0.2, + height: 40, + child: TextField( + controller: searchController, + onChanged: filterHotels, + decoration: InputDecoration( + hintText: "Search ...", + hintStyle: TextStyle( + fontSize: 12, color: Color(0xFF9E9DBD)), + prefixIcon: Icon( + Icons.search, + color: Color(0xFF9E9DBD), + size: 18, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade200, width: 0.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade300, width: 1), + ), + ), + style: GoogleFonts.poppins( + fontSize: 12, + ), + ), + ), + // SizedBox(width: 16), + Spacer(), + + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF114D8B), + foregroundColor: Colors.white, + disabledBackgroundColor: Color(0xFF114D8B), + disabledForegroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: + BorderSide(color: Color(0xFF114D8B), width: 2), + ), + padding: EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + ), + onPressed: () async { + showDialog( + context: context, + builder: (context) => HotelsData( + isDesktop: isDesktop, + layoutColor: layoutColor!, + fetchGetHotels: refreshData, + // role: + // "Travel Agent" + ), + ); + }, + child: Row( + mainAxisSize: + MainAxisSize.min, // Ensures content fits nicely + children: [ + Text( + "Add Hotels", + style: GoogleFonts.poppins( + fontSize: isDesktop ? 13 : 11, + ), + ), + SizedBox(width: 8), // spacing between icon and text + Icon( + Icons.add_circle_outline_rounded, + size: 15, + color: Colors.white, + ), + ], + ), + ), + ], + ), + + if (!isDesktop) + SizedBox( + height: 5, + ), + isDesktop + ? SizedBox.shrink() + : Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.8, + height: 35, + child: TextField( + controller: searchController, + onChanged: filterHotels, + decoration: InputDecoration( + hintText: "Search ...", + hintStyle: TextStyle( + fontSize: 12, color: Color(0xFF9E9DBD)), + prefixIcon: Icon( + Icons.search, + color: Color(0xFF9E9DBD), + size: 18, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade200, + width: 0.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Colors.grey.shade300, width: 1), + ), + ), + style: GoogleFonts.poppins( + fontSize: 12, + ), + ), + ), + // SizedBox(width: 16), + ], + ), + const SizedBox(height: 10), + FutureBuilder>( + future: futureHotels, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError || + !snapshot.hasData || + snapshot.data!.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // const Icon(Icons.error_outline, + // color: Colors.redAccent, size: 60), + // const SizedBox(height: 16), + // Text( + // "Oops!", + // style: GoogleFonts.poppins( + // fontSize: 20, + // fontWeight: FontWeight.bold, + // color: Colors.redAccent), + // ), + const SizedBox(height: 8), + Text( + "No Hotels Available ", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.grey), + ), + const SizedBox(height: 20), + Text( + "Please Create Hotels", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 16, color: Colors.grey), + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + List hotels = + filteredHotels.isNotEmpty ? filteredHotels : allHotels; + + hotels.sort((a, b) { + DateTime dateA = DateTime.parse(a['created_on']); + DateTime dateB = DateTime.parse(b['created_on']); + + return dateB + .compareTo(dateA); // Descending: newest first + }); + + List paginatedHotels = hotels + .skip(currentPage * itemsPerPage) + .take(itemsPerPage) + .toList(); + + Widget table = LayoutBuilder( + builder: (context, constraints) { + double minWidth = + isDesktop ? constraints.maxWidth : 1300; + + return ConstrainedBox( + constraints: BoxConstraints(minWidth: minWidth), + child: DataTable( + dividerThickness: 0.5, + columnSpacing: isDesktop ? 24.0 : 16.0, + border: TableBorder( + horizontalInside: BorderSide( + width: 0.5, color: Colors.grey.shade200), + ), + columns: [ + DataColumn( + label: Text( + 'Hotel Name', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w600), + )), + DataColumn( + label: Text( + 'Hotel Chain', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w600), + )), + DataColumn( + label: Text( + 'City', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w600), + )), + DataColumn( + label: Text( + 'Country', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w600), + )), + DataColumn( + label: Text( + 'Status', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w600), + )), + DataColumn( + label: Text( + 'Actions', + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w600), + )), + ], + rows: paginatedHotels.map((hotels) { + String hotelsId = hotels['hotel_id'] + .toString(); // Get user ID + bool isSelected = selectedUserId == hotelsId; + + return DataRow(cells: [ + DataCell(Text(hotels['hotel_name'] ?? 'N/A', + style: TextStyle( + fontSize: 13, + fontFamily: "Inter", + ), + softWrap: true, + overflow: TextOverflow.ellipsis)), + DataCell(Text(hotels['hotel_chain'] ?? '', + style: TextStyle( + fontSize: 13, + fontFamily: "Inter", + ))), + DataCell(Text(hotels['city'] ?? 'N/A', + style: TextStyle( + fontSize: 13, + fontFamily: "Inter", + ), + softWrap: true, + overflow: TextOverflow.ellipsis)), + DataCell(Text(hotels['country_name'] ?? '', + style: TextStyle( + fontSize: 13, + fontFamily: "Inter", + ))), + DataCell( + Text( + hotels['is_active'] == "1" + ? 'Active' + : 'Inactive', + style: TextStyle( + fontSize: 13, + fontFamily: "Inter", + color: hotels['is_active'] == "1" ? Colors.green : Colors.red, + ), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + ), + DataCell( + // UserActionsMenu( + // user: hotels, + // getUserDetails: (id) => + // apiService.getSingleUser(id), + // ), + GestureDetector( + child: Image.asset( + 'assets/images/IconsImg/edit.png', + width: 20, + height: 15), + onTap: () async { + // final userId = getUserId(user['user_id']); + // final usersData = await getUserDetails(userId); + // + final hotelsId = int.tryParse( + hotels['hotel_id'] + .toString()); + + if (hotelsId != null) { + print("HotelsId -- $hotelsId"); + final data = await apiService + .getHotelsDetailsFind(hotelsId); + print("HotelsId -- $data"); + + showDialog( + context: context, + builder: (context) => HotelsData( + isDesktop: isDesktop, + hotelsId: hotelsId, // Pass the ID + hotelsData: data, + layoutColor: layoutColor!, + // fetchGetHotels: fetchGetHotels, + fetchGetHotels: refreshData, + // role: + // "Travel Agent" + ), + ); + } else { + print("Invalid Hotels ID"); + } + }, + ), + ), + ]); + }).toList(), + ), + ); + }, + ); + + Widget buildMobileCardView(List paginatedUser) { + return ListView.builder( + itemCount: paginatedUser.length, + itemBuilder: (context, index) { + final hotels = paginatedUser[index]; + return Card( + color: Colors.white, + margin: EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 3, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status and Employee Code + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + hotels['hotel_name'] ?? 'N/A', + style: GoogleFonts.poppins( + fontSize: 10, + color: Colors.black87, + fontWeight: FontWeight.w700), + ), + + GestureDetector( + child: Image.asset( + 'assets/images/IconsImg/edit.png', + width: 20, + height: 15), + onTap: () async { + // final userId = getUserId(user['user_id']); + // final usersData = await getUserDetails(userId); + // + final hotelsId = int.tryParse( + hotels['hotel_id'] + .toString()); + + if (hotelsId != null) { + print("HotelsId -- $hotelsId"); + final data = await apiService + .getHotelsDetailsFind(hotelsId); + print("HotelsId -- $data"); + + showDialog( + context: context, + builder: (context) => HotelsData( + isDesktop: isDesktop, + hotelsId: + hotelsId, // Pass the ID + hotelsData: data, + layoutColor: layoutColor!, + // fetchGetHotels: fetchGetHotels, + fetchGetHotels: refreshData, + // role: + // "Travel Agent" + ), + ); + } else { + print("Invalid Hotels ID"); + } + }, + ), + ], + ), + + SizedBox(height: 2), + // Trip Id and Trip Name + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${hotels['hotel_chain'] ?? ''} ", + style: GoogleFonts.poppins( + fontSize: 12, + color: Colors.black87, + fontWeight: FontWeight.w500), + ), + ], + ), + ], + ), + SizedBox(height: 2), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${hotels['city'] ?? ''} ", + style: GoogleFonts.poppins( + fontSize: 12, + color: Colors.black87, + fontWeight: FontWeight.w500), + ), + ], + ), + ], + ), + SizedBox(height: 2), + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + hotels['country_name'] ?? '', + style: GoogleFonts.poppins( + fontSize: 10, + color: Colors.black87), + ), + ], + ), + ], + ), + // ** + ], + ), + ), + ); + }, + ); + } + + return Expanded( + child: Column( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: isDesktop + ? (searchController.text.isNotEmpty && + filteredHotels.isEmpty + ? Center( + child: Text( + "No matches found", + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey), + ), + ) + : SingleChildScrollView( + scrollDirection: Axis.vertical, + child: table, + )) + : (searchController.text.isNotEmpty && + filteredHotels.isEmpty + ? Center( + child: Text( + "No matches found", + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.grey), + ), + ) + : buildMobileCardView(paginatedHotels)), + ), + // Expanded( + // child: isDesktop + // ? SingleChildScrollView( + // scrollDirection: Axis.vertical, + // child: table, // <-- your existing table + // ) + // : buildMobileCardView(paginatedUser), + // ), + PaginationControls( + currentPage: currentPage, + itemsPerPage: itemsPerPage, + totalItems: hotels.length, + activeColor: layoutColor, // your theme color + onPageChanged: (page) { + setState(() { + currentPage = page; + }); + }, + onItemsPerPageChanged: (items) { + setState(() { + itemsPerPage = items; + currentPage = 0; + }); + }, + ), + ], + ), + ); + }, + ) + ]), + )), + ); + } +} diff --git a/lib/routes/custom_router.dart b/lib/routes/custom_router.dart index f6f26b1..ea259bf 100644 --- a/lib/routes/custom_router.dart +++ b/lib/routes/custom_router.dart @@ -27,6 +27,7 @@ import '../Screens/userManagement/create_user/create_user.dart'; import '../Screens/department/department_list.dart'; import '../Screens/costCenter/costCenter_list.dart'; import '../Screens/dashboard/status_dashboard.dart'; +import '../Screens/hotels/hotels_list.dart'; final GoRouter router = GoRouter( routes: [ @@ -137,6 +138,10 @@ final GoRouter router = GoRouter( path: '/costcenter', builder: (context, state) => CostCenterList(), ), + GoRoute( + path: '/hotels', + builder: (context, state) => HotelsDataList(), + ), GoRoute( path: '/statusdashboard', builder: (context, state) => StatusDashboard(), diff --git a/lib/routes/organizationSetting.dart b/lib/routes/organizationSetting.dart index ac1c01d..35ea714 100644 --- a/lib/routes/organizationSetting.dart +++ b/lib/routes/organizationSetting.dart @@ -94,6 +94,12 @@ class OrganizationSettingState extends State { 'label': 'Cost Center', 'description': 'Create and Edit Cost Center' }, + { + 'value': '/hotels', + 'icon': Icons.cabin_sharp, + 'label': 'Hotels', + 'description': 'Create and Edit Hotels' + }, ]; List rows = []; @@ -119,6 +125,7 @@ class OrganizationSettingState extends State { case '/getPerdiem': case '/templateList': case '/costcenter': + case '/hotels': context.go(route); break; default: diff --git a/lib/services/apiService.dart b/lib/services/apiService.dart index b4d07e0..15c20a0 100644 --- a/lib/services/apiService.dart +++ b/lib/services/apiService.dart @@ -998,4 +998,49 @@ class ApiService { } } + Future> getHotelsDetailsFind(int id) async { + final String apiUrldata = '$apiUrl/api/findHotels?hotel_id=$id'; + + 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('findout the result'); + // print(data.runtimeType); + // print(data); + + if (!data.containsKey('data') || data['data'] is! List) { + throw Exception( + "Invalid response format: 'data' field is missing or not a List"); + } + + final List> listData = + List>.from(data['data']); + + if (listData.isEmpty) { + throw Exception("No Hotel data found with ID $id"); + } + + return listData[0]; + } catch (e) { + throw Exception('Error parsing response: $e'); + } + } else { + throw Exception('Failed to load Hotel details'); + } + } + }