diff --git a/assets/images/unlimited_card_details.png b/assets/images/unlimited_card_details.png new file mode 100644 index 0000000..1345db4 Binary files /dev/null and b/assets/images/unlimited_card_details.png differ diff --git a/lib/attractions/widget/attraction_card.dart b/lib/attractions/widget/attraction_card.dart index eb8abf8..170c985 100644 --- a/lib/attractions/widget/attraction_card.dart +++ b/lib/attractions/widget/attraction_card.dart @@ -24,7 +24,7 @@ class AttractionCard extends StatelessWidget { onTap: () { Navigator.of(context).pushNamed( RouteConstants.attractionDetails, - arguments: attraction, + arguments: attraction.id, ); }, child: Container( diff --git a/lib/buy_a_pass/view/buy_pass_view.dart b/lib/buy_a_pass/view/buy_pass_view.dart index 32905e5..d0169fc 100644 --- a/lib/buy_a_pass/view/buy_pass_view.dart +++ b/lib/buy_a_pass/view/buy_pass_view.dart @@ -401,10 +401,10 @@ class BuyPassContent extends StatelessWidget { ), child: GestureDetector( onTap: () { - // Navigator.of(context).pushNamed( - // RouteConstants.attractionDetails, - // arguments: attraction, - // ); + Navigator.of(context).pushNamed( + RouteConstants.attractionDetails, + arguments: attraction.id, + ); }, child: ClipRRect( borderRadius: BorderRadius.circular(8.r), diff --git a/lib/common_packages/custom_dash_border_painter.dart b/lib/common_packages/custom_dash_border_painter.dart new file mode 100644 index 0000000..40f488d --- /dev/null +++ b/lib/common_packages/custom_dash_border_painter.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class DashedBorderPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double gap; + final double dashWidth; + final double radius; + + DashedBorderPainter({ + required this.color, + this.strokeWidth = 1.5, + this.gap = 6, + this.dashWidth = 6, + this.radius = 16, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; + + final rRect = RRect.fromRectAndRadius( + Offset.zero & size, + Radius.circular(radius), + ); + + final path = Path()..addRRect(rRect); + + final dashPath = Path(); + for (final metric in path.computeMetrics()) { + double distance = 0; + while (distance < metric.length) { + dashPath.addPath( + metric.extractPath(distance, distance + dashWidth), + Offset.zero, + ); + distance += dashWidth + gap; + } + } + + canvas.drawPath(dashPath, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 8079eba..ea01376 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -15,6 +15,8 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_s import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_view.dart'; import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_empty_view.dart'; import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart'; +import 'package:citycards_customer/my_pass/views/pass_attractions_page_view.dart'; +import 'package:citycards_customer/my_pass/views/search_pass_offers_with_listing.dart'; import 'package:citycards_customer/offer_pass_detail/offer_pass_detail_view.dart'; import 'package:citycards_customer/search_offers/bloc/search_offers_listing_bloc.dart'; import 'package:citycards_customer/search_offers/view/search_offers_with_listing.dart'; @@ -28,6 +30,7 @@ import '../cart/views/my_cart_view_page.dart'; import '../common_bloc/bottom_navigation_bloc.dart'; import '../home/views/home_page_view.dart'; import '../home/views/registered_user_home_page.dart'; +import '../my_pass/views/pass_attraction_details_view.dart'; import '../profile/view/contact_us/contact_us_view.dart'; import '../profile/view/edit_profile/edit_profile_view.dart'; import '../profile/view/faq/faq_view.dart'; @@ -70,6 +73,9 @@ class AppRouter { case RouteConstants.attractionsPage: final args = settings.arguments as String; return MaterialPageRoute(builder: (_) => AttractionsPage(source: args)); + case RouteConstants.passAttractionsPage: + final args = settings.arguments as String; + return MaterialPageRoute(builder: (_) => PassAttractionsPage(source: args)); case RouteConstants.profile: return MaterialPageRoute( builder: (_) { @@ -150,10 +156,18 @@ class AppRouter { ); case RouteConstants.attractionDetails: - final attractionId = settings.arguments as Attraction; + final attractionId = settings.arguments as int; return MaterialPageRoute( builder: (_) { - return AttractionDetailsView(attractionId: attractionId.id,); + return AttractionDetailsView(attractionId: attractionId); + }, + ); + + case RouteConstants.passAttractionDetails: + final attractionID = settings.arguments as int; + return MaterialPageRoute( + builder: (_) { + return PassAttractionDetailsView(attractionId: attractionID); }, ); @@ -190,6 +204,15 @@ class AppRouter { ); }, ); + case RouteConstants.searchPassOffer: + return MaterialPageRoute( + builder: (_) { + return BlocProvider( + create: (_) => OffersBloc(OffersRepository()), + child: PassOffersScreen(), + ); + }, + ); case RouteConstants.addDetails: final bookingId = settings.arguments as int; diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart index 8173f63..a63601d 100644 --- a/lib/core/inside_bottom_navigator.dart +++ b/lib/core/inside_bottom_navigator.dart @@ -2,6 +2,9 @@ import 'package:citycards_customer/attractions/models/attraction_model.dart'; import 'package:citycards_customer/core/route_constants.dart'; import 'package:citycards_customer/home/views/registered_user_home_page.dart'; import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart'; +import 'package:citycards_customer/my_pass/views/pass_attraction_details_view.dart'; +import 'package:citycards_customer/my_pass/views/pass_attractions_page_view.dart'; +import 'package:citycards_customer/my_pass/views/search_pass_offers_with_listing.dart'; import 'package:citycards_customer/postcard/views/add_filter_step_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -18,10 +21,11 @@ import '../itinerary_creation/views/itinerary_creation_view.dart'; import '../itinerary_creation/views/magic_itinerary_view.dart'; import '../my_pass/views/booking_page_view.dart'; import '../my_pass/views/booking_successful_page_view.dart'; -import '../my_pass/views/qr_pass_page_view.dart'; +import '../my_pass/views/pass_details_page_view.dart'; import '../offer_pass_detail/offer_pass_detail_view.dart'; import '../postcard/blocs/postcard_creation_bloc.dart'; import '../postcard/views/postcard_creation_page_view.dart'; +import '../profile/view/privacy/privacy_view.dart'; import '../search_offers/bloc/offers_bloc.dart'; import '../search_offers/bloc/search_offers_listing_bloc.dart'; import '../search_offers/repository/offers_repository.dart'; @@ -54,12 +58,25 @@ Widget buildOffstageNavigator( return MaterialPageRoute( builder: (_) => AttractionsPage(source: args), ); + case RouteConstants.passAttractionsPage: + final args = settings.arguments as String; + return MaterialPageRoute( + builder: (_) => PassAttractionsPage(source: args), + ); case RouteConstants.attractionDetails: - final attraction = settings.arguments as Attraction; + final attractionID = settings.arguments as int; return MaterialPageRoute( builder: (_) { - return AttractionDetailsView(attractionId: attraction.id); + return AttractionDetailsView(attractionId: attractionID); + }, + ); + + case RouteConstants.passAttractionDetails: + final attractionID = settings.arguments as int; + return MaterialPageRoute( + builder: (_) { + return PassAttractionDetailsView(attractionId: attractionID); }, ); @@ -99,6 +116,22 @@ Widget buildOffstageNavigator( ); }, ); + case RouteConstants.searchPassOffer: + return MaterialPageRoute( + builder: (_) { + return BlocProvider( + create: (_) => OffersBloc(OffersRepository()), + child: PassOffersScreen(), + ); + }, + ); + + case RouteConstants.privacyPolicy: + return MaterialPageRoute( + builder: (_) { + return const PrivacyPolicyPage(); + }, + ); // πŸ”Ή Upload Photo Page (start of postcard creation flow) case RouteConstants.uploadPhotoPage: @@ -129,7 +162,7 @@ Widget buildOffstageNavigator( final previousBloc = BlocProvider.of(context); return BlocProvider.value( value: previousBloc, - child: const QrPassView(), + child: const PassDetailsView(), ); }, ); diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index 0d8c270..d4ac16b 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -9,6 +9,7 @@ class RouteConstants { static const String home = '/home'; static const String registeredUserHome = '/registeredUserHome'; static const String attractionsPage = "/attractions"; + static const String passAttractionsPage = "/passAttractionsPage"; static const String postCardPage = "/postcards"; static const String uploadPhotoPage = "/uploadPhoto"; static const String addFilterPage = "/addFilter"; @@ -37,12 +38,14 @@ class RouteConstants { /**************************** Attraction Page *****************************************/ static const String attractionDetails ='/attractionDetails'; + static const String passAttractionDetails ='/passAttractionDetails'; /**************************** By Pass Page Page *****************************************/ static const String buyPass ='/buyPass'; static const String checkout ='/checkout'; static const String searchOffer = '/searchOffer'; + static const String searchPassOffer = '/searchPassOffer'; static const String createAcct = '/createAcct'; static const String addDetails = '/addDetails'; static const String offerPassDetail = "/offerPassDetail"; diff --git a/lib/create_account/bloc/create_account_bloc.dart b/lib/create_account/bloc/create_account_bloc.dart index cd62f97..b808abe 100644 --- a/lib/create_account/bloc/create_account_bloc.dart +++ b/lib/create_account/bloc/create_account_bloc.dart @@ -28,14 +28,21 @@ class CreateAccountBloc extends Bloc { mobileNumber: event.mobileNumber, address1: event.address1, address2: event.address2, + city: event.city, + state: event.state, + country: event.country, + postalCode: event.postalCode, ); + await LocalPreference.setLogin(true); + // βœ… FIX: Parse directly from response, just like verify OTP + final userModel = UserRegisteredModel.fromJson(response); - final userModel = UserRegisteredModel.fromJson(response['data'] ?? {}); await LocalPreference.setTokens( accessToken: userModel.accessToken, refreshToken: userModel.refreshToken, refreshTokenMaxAge: userModel.refreshTokenMaxAge, ); + await LocalPreference.setUserDetails( userId: userModel.user.id, firstName: userModel.user.firstName, @@ -45,10 +52,12 @@ class CreateAccountBloc extends Bloc { role: userModel.user.role, roleId: userModel.user.roleId, ); + await LocalPreference.setProfileImage(userModel.user.profileImage); + emit(CreateAccountSuccess( - message: response['message'] ?? 'Account created successfully', - userData: response['data'] ?? {}, + message: 'Account created successfully', + userData: response, )); } catch (e) { emit(CreateAccountFailure( @@ -63,4 +72,4 @@ class CreateAccountBloc extends Bloc { ) { emit(const CreateAccountInitial()); } -} \ No newline at end of file +} diff --git a/lib/create_account/bloc/create_account_event.dart b/lib/create_account/bloc/create_account_event.dart index 5bd6fd7..26a484b 100644 --- a/lib/create_account/bloc/create_account_event.dart +++ b/lib/create_account/bloc/create_account_event.dart @@ -14,6 +14,10 @@ class CreateAccountSubmitted extends CreateAccountEvent { final String mobileNumber; final String address1; final String address2; + final String city; + final String state; + final String country; + final String postalCode; const CreateAccountSubmitted({ required this.firstName, @@ -22,6 +26,10 @@ class CreateAccountSubmitted extends CreateAccountEvent { required this.mobileNumber, required this.address1, required this.address2, + required this.city, + required this.state, + required this.country, + required this.postalCode, }); @override @@ -32,9 +40,13 @@ class CreateAccountSubmitted extends CreateAccountEvent { mobileNumber, address1, address2, + city, + state, + country, + postalCode, ]; } class CreateAccountReset extends CreateAccountEvent { const CreateAccountReset(); -} \ No newline at end of file +} diff --git a/lib/create_account/repository/create_account_repository.dart b/lib/create_account/repository/create_account_repository.dart index 738f7d4..2f54d8c 100644 --- a/lib/create_account/repository/create_account_repository.dart +++ b/lib/create_account/repository/create_account_repository.dart @@ -11,17 +11,25 @@ class CreateAccountRepository { required String mobileNumber, required String address1, required String address2, + required String city, + required String state, + required String country, + required String postalCode, }) async { try { final response = await _apiServices.postApi( url: ApiUrls.createAccount, data: { - 'firstName': firstName, - 'lastName': lastName, - 'emailAddress': emailAddress, - 'mobileNumber': mobileNumber, - 'address1': address1, - 'address2': address2, + "firstName": firstName, + "lastName": lastName, + "emailAddress": emailAddress, + "mobileNumber": mobileNumber, + "address1": address1, + "address2": address2, + "city": city, + "state": state, + "country": country, + "postalCode": postalCode, }, ); @@ -30,4 +38,4 @@ class CreateAccountRepository { throw Exception('Failed to create account: $e'); } } -} \ No newline at end of file +} diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart index be78665..e492639 100644 --- a/lib/create_account/view/create_account_view.dart +++ b/lib/create_account/view/create_account_view.dart @@ -5,7 +5,11 @@ import 'package:citycards_customer/common_packages/custom_textfield.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../core/route_constants.dart'; +import '../../itinerary_creation/bloc/get_itinerary_bloc.dart'; import '../../localPreference/local_preference.dart'; +import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart'; +import '../../postcard/blocs/myPostCards/my_postcard_event.dart'; import '../../profile/bloc/profile/profile_bloc.dart'; import '../../profile/bloc/profile/profile_event.dart'; import '../bloc/create_account_bloc.dart'; @@ -22,16 +26,24 @@ class CreateAccountView extends StatelessWidget { final TextEditingController emailController = TextEditingController(); final TextEditingController phoneController = TextEditingController(); final TextEditingController addressController = TextEditingController(); + final TextEditingController cityController = TextEditingController(); + final TextEditingController stateController = TextEditingController(); + final TextEditingController countryController = TextEditingController(); + final TextEditingController postalController = TextEditingController(); void _submitForm(BuildContext context) { if (firstNameController.text.trim().isEmpty || lastNameController.text.trim().isEmpty || emailController.text.trim().isEmpty || phoneController.text.trim().isEmpty || - addressController.text.trim().isEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Please fill all fields'))); + addressController.text.trim().isEmpty || + cityController.text.trim().isEmpty || + stateController.text.trim().isEmpty || + countryController.text.trim().isEmpty || + postalController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please fill all fields')), + ); return; } @@ -43,6 +55,10 @@ class CreateAccountView extends StatelessWidget { mobileNumber: phoneController.text.trim(), address1: addressController.text.trim(), address2: '', + city: cityController.text.trim(), + state: stateController.text.trim(), + country: countryController.text.trim(), + postalCode: postalController.text.trim(), ), ); } @@ -56,14 +72,19 @@ class CreateAccountView extends StatelessWidget { child: BlocListener( listener: (ctx, state) async { if (state is CreateAccountSuccess) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(state.message))); await LocalPreference.setLogin(true); final userId = await LocalPreference.getUserId(); context.read().add(FetchProfileEvent(userId: userId!)); context.read().add(CheckLoginStatusEvent()); + context.read().add(CheckLoginStatus()); + context.read().add(CheckLoginAndFetchItinerary()); + // context.read().add(FetchDraftPostCards()); + context.read().add(RefreshDraftPostCards()); + context.read().add(RefreshOrderPostCards()); Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.message))); } else if (state is CreateAccountFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -168,14 +189,45 @@ class CreateAccountView extends StatelessWidget { Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Address 1", + label: "Address", hint: "Enter address manually or tap to search", controller: addressController, ), ), - + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "City", + hint: "Enter your city", + controller: cityController, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "State", + hint: "Enter your state", + controller: stateController, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Country", + hint: "Enter your country", + controller: countryController, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Postal Code", + hint: "Enter postal / zip code", + controller: postalController, + keyboardType: TextInputType.number, + ), + ), SizedBox(height: 20.h), - BlocBuilder( builder: (context, state) { if (state is CreateAccountLoading) { diff --git a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart index 23e850b..bf0cb73 100644 --- a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart +++ b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart @@ -23,18 +23,21 @@ class GetItineraryBloc extends Bloc { try { emit(GetItineraryLoading()); - // Check login status final isLoggedIn = await LocalPreference.getLogin(); - // Uncomment above and remove below line when ready for production - // final isLoggedIn = true; // For testing if (!isLoggedIn) { emit(GetItineraryNotLoggedIn()); return; } - // If logged in, fetch itineraries final response = await _repository.fetchMyItineraries(); + + // Check if user has unlimited pass + if (!response.isUnlimitedPass) { + emit(GetItineraryRequiresPass(itineraries: response.itineraries)); + return; + } + emit(GetItinerarySuccessfully(itineraries: response.itineraries)); } catch (e) { emit(GetItineraryFailed( @@ -53,6 +56,12 @@ class GetItineraryBloc extends Bloc { final response = await _repository.fetchMyItineraries(); + // Check if user has unlimited pass + if (!response.isUnlimitedPass) { + emit(GetItineraryRequiresPass(itineraries: response.itineraries)); + return; + } + emit(GetItinerarySuccessfully(itineraries: response.itineraries)); } catch (e) { emit(GetItineraryFailed( diff --git a/lib/itinerary_creation/bloc/get_itinerary_state.dart b/lib/itinerary_creation/bloc/get_itinerary_state.dart index 616e7a9..4f9afc8 100644 --- a/lib/itinerary_creation/bloc/get_itinerary_state.dart +++ b/lib/itinerary_creation/bloc/get_itinerary_state.dart @@ -22,6 +22,15 @@ class GetItinerarySuccessfully extends GetItineraryState { List get props => [itineraries]; } +class GetItineraryRequiresPass extends GetItineraryState { + final List itineraries; + + const GetItineraryRequiresPass({required this.itineraries}); + + @override + List get props => [itineraries]; +} + class GetItineraryFailed extends GetItineraryState { final String error; diff --git a/lib/itinerary_creation/views/magic_itinerary_view.dart b/lib/itinerary_creation/views/magic_itinerary_view.dart index 68ea535..1af2f78 100644 --- a/lib/itinerary_creation/views/magic_itinerary_view.dart +++ b/lib/itinerary_creation/views/magic_itinerary_view.dart @@ -9,6 +9,7 @@ import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../login/view/login_email_bottomsheet.dart'; import 'package:intl/intl.dart'; @@ -43,7 +44,6 @@ class _MagicItineraryViewState extends State { showDivider: false, ), SizedBox(height: 24.h), - // BLoC Builder for all states BlocBuilder( builder: (context, state) { @@ -56,6 +56,8 @@ class _MagicItineraryViewState extends State { ); } else if (state is GetItineraryNotLoggedIn) { return NotLoggedInItineraryView(); + } else if (state is GetItineraryRequiresPass) { + return RequiresUnlimitedPassView(); } else if (state is GetItinerarySuccessfully) { if (state.itineraries.isEmpty) { return NoItineraryView(); @@ -192,8 +194,8 @@ class NotLoggedInItineraryView extends StatelessWidget { } } -class NoItineraryView extends StatelessWidget { - const NoItineraryView({super.key}); +class RequiresUnlimitedPassView extends StatelessWidget { + const RequiresUnlimitedPassView({super.key}); @override Widget build(BuildContext context) { @@ -201,17 +203,17 @@ class NoItineraryView extends StatelessWidget { children: [ SizedBox(height: 40.h), - // Illustration for no itineraries - Icon( - Icons.travel_explore, - size: 120.sp, - color: Colors.grey.withOpacity(0.3), + // Illustration image + Image.asset( + "assets/images/no_itinerary.png", // Update with your actual asset path + height: 300.h, + fit: BoxFit.contain, ), SizedBox(height: 32.h), CustomText( - text: "No Itineraries Yet", + text: "You do not possess an Unlimited Pass! πŸ˜”", size: 18.sp, weight: FontWeight.w600, textAlign: TextAlign.center, @@ -222,8 +224,7 @@ class NoItineraryView extends StatelessWidget { Padding( padding: EdgeInsets.symmetric(horizontal: 24.w), child: CustomText( - text: - "You haven't created any itineraries yet. Start planning your next adventure!", + text: "Get your Unlimited Pass and create a custom itinerary!", size: 14.sp, color: Color(0xFF656565), textAlign: TextAlign.center, @@ -234,14 +235,9 @@ class NoItineraryView extends StatelessWidget { CustomFilledButton( onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ItineraryCreationStartPage(), - ), - ); + context.read().add(NavigationTabChanged(0)); }, - label: "Create My Itinerary", + label: "Buy Unlimited CityCard", showArrow: true, ), ], @@ -249,6 +245,70 @@ class NoItineraryView extends StatelessWidget { } } +class NoItineraryView extends StatelessWidget { + const NoItineraryView({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + children: [ + SizedBox(height: 60.h), + + /// Illustration Image + Center( + child: Image.asset( + "assets/images/no_itinerary.png", + height: 260.h, + fit: BoxFit.contain, + ), + ), + + SizedBox(height: 32.h), + + /// Title + CustomText( + text: "You Don’t have an Itinerary Yet! 😟", + size: 18.sp, + weight: FontWeight.w600, + textAlign: TextAlign.center, + ), + + SizedBox(height: 12.h), + + /// Subtitle + CustomText( + text: + "Create your own personalized magic itinerary that suites your travel needs", + size: 14.sp, + color: const Color(0xFF656565), + textAlign: TextAlign.center, + ), + + SizedBox(height: 32.h), + + /// Button + CustomFilledButton( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ItineraryCreationStartPage(), + ), + ); + }, + label: "Create My Itinerary", + showArrow: true, + ), + + SizedBox(height: 40.h), + ], + ), + ); + } +} + class ErrorItineraryView extends StatelessWidget { final String error; final VoidCallback onRetry; diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart index 262ad3f..4242dd6 100644 --- a/lib/login/view/verify_otp_bottomsheet.dart +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -58,7 +58,7 @@ class _VerifyOtpBottomsheetState extends State { ); } else { // User doesn't exist - navigate to create account - Navigator.of(context).pushReplacementNamed(RouteConstants.createAcct,arguments: widget.emailAddress); + Navigator.of(context).pushNamed(RouteConstants.createAcct,arguments: widget.emailAddress); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please complete your profile'), diff --git a/lib/my_pass/views/pass_attraction_details_view.dart b/lib/my_pass/views/pass_attraction_details_view.dart new file mode 100644 index 0000000..032477e --- /dev/null +++ b/lib/my_pass/views/pass_attraction_details_view.dart @@ -0,0 +1,594 @@ +import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart'; +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:latlong2/latlong.dart'; +import '../../attraction_details/bloc/attraction_details_bloc.dart'; +import '../../attraction_details/bloc/attraction_details_event.dart'; +import '../../attraction_details/bloc/attraction_details_state.dart'; +import '../../attraction_details/repository/attraction_details_repository.dart'; +import '../../core/route_constants.dart'; + +class PassAttractionDetailsView extends StatelessWidget { + final int? attractionId; + + const PassAttractionDetailsView({ + super.key, + required this.attractionId, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => AttractionDetailsBloc( + repository: AttractionDetailsRepository(), + )..add(FetchAttractionDetails(attractionId: attractionId??0)), + child: BlocBuilder( + builder: (context, state) { + if (state is AttractionDetailsLoading) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + + if (state is AttractionDetailsError) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Text( + state.message, + style: TextStyle(color: Colors.red), + ), + ), + ); + } + + if (state is AttractionDetailsLoaded) { + final attraction = state.attractionDetails; + final coverImage = attraction.attractionGalleries + .firstWhere( + (gallery) => gallery.isCoverImage, + orElse: () => attraction.attractionGalleries.first, + ) + .filePathUrl; + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Image.network( + coverImage, + height: 377.h, + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + 'assets/images/koh_rong_samloem_banner.png', + height: 377.h, + width: double.infinity, + fit: BoxFit.cover, + ); + }, + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 20.w, vertical: 10.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: true, + isProfilePage: false, + showDivider: true, + ), + SizedBox(height: 10.h), + Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon( + Icons.arrow_back, + size: 24.sp, + color: Colors.white, + ), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + attraction.title, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ), + ), + Positioned( + bottom: 31.h, + left: 12.w, + right: 60.w, // Add this - leaves space for share button + child: Text( + attraction.title, + style: TextStyle( + color: Colors.white, + fontSize: 44.sp, + fontWeight: FontWeight.w500, + height: 1.2, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Positioned( + bottom: 31.h, + right: 17.w, + child: GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + const ShareBottomSheet(), + ); + }, + child: Container( + height: 36.h, + width: 36.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20.r), + ), + child: Center( + child: Icon( + Icons.share_sharp, + color: Colors.black, + size: 18.sp, + ), + ), + ), + ), + ), + ], + ), + + // About Section + Padding( + padding: + EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "About", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 12.32.h), + Text( + attraction.description, + style: TextStyle( + color: Color(0xFF262626), + fontWeight: FontWeight.w400, + fontSize: 14.sp, + height: 1.5, + ), + ), + ], + ), + ), + SizedBox(height: 41.h), + + // Booking Section + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "How to make a booking?", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Color(0xFFF95F62)), + ), + child: Row( + children: [ + Icon( + Icons.call, + color: Color(0xFFF95F62), + size: 32.w, + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CustomText( + text: "Contact Number", + color: Colors.black.withOpacity(.6), + size: 12.sp, + weight: FontWeight.w500, + ), + SizedBox(height: 6.h), + CustomText( + text: attraction.bookingPhoneNumber??"N/A", + color: Colors.black, + size: 14.sp, + weight: FontWeight.w600, + ), + SizedBox(height: 6.h), + CustomText( + text: "Tap to call", + color: Colors.black.withOpacity(.4), + size: 12.sp, + weight: FontWeight.w400, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Color(0xFFF95F62)), + ), + child: Row( + children: [ + Icon( + Icons.email_sharp, + color: Color(0xFFF95F62), + size: 32.w, + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CustomText( + text: "Email", + color: Colors.black.withOpacity(.6), + size: 12.sp, + weight: FontWeight.w500, + ), + SizedBox(height: 6.h), + CustomText( + text: attraction.bookingEmail??"N/A", + color: Colors.black, + size: 14.sp, + weight: FontWeight.w600, + ), + SizedBox(height: 6.h), + CustomText( + text: "Tap to email", + color: Colors.black.withOpacity(.4), + size: 12.sp, + weight: FontWeight.w400, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + InkWell( + onTap: () { + Navigator.of(context) + .pushNamed(RouteConstants.makeBooking); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 24.w, + vertical: 18.h, + ), + decoration: BoxDecoration( + color: Color(0xFFF95F62), + borderRadius: BorderRadius.circular(10.r), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CustomText( + text: "Via CityCards", + size: 16.sp, + weight: FontWeight.w500, + color: Colors.white, + ), + SizedBox(height: 8.h), + CustomText( + text: "Create a booking via app", + size: 11.sp, + weight: FontWeight.w400, + color: Colors.white, + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios_outlined, + color: Colors.white, + ), + ], + ), + ), + ), + SizedBox(height: 30.h), + Divider(color: Colors.black.withOpacity(0.2)), + SizedBox(height: 30.h), + Text( + "What is included", + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 4.h), + + // Dynamic Inclusions from API + Wrap( + runSpacing: 16.h, + spacing: 16.w, + children: attraction.attractionInclusions + .where((inclusion) => inclusion.isInclusion) + .map( + (inclusion) => includedBox( + "assets/icons/bus.png", + inclusion.title, + inclusion.description, + ), + ) + .toList(), + ), + SizedBox(height: 30.h), + // Divider(color: Colors.black.withOpacity(0.2)), + SizedBox(height: 30.h), + Text( + "Exact Location", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 8.h), + CustomText( + text: "View the location on map", + size: 12.sp, + color: Colors.black.withOpacity(.6), + ), + SizedBox(height: 17.h), + Container( + height: 178.7.h, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(13.54.r), + border: Border.all( + color: Colors.grey.withOpacity(0.3), + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(13.54.r), + child: FlutterMap( + options: MapOptions( + initialCenter: LatLng( + attraction.latitudeCoordinate, + attraction.longitudeCoordinate, + ), + initialZoom: 15.0, + interactionOptions: InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + ), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.example.citycards_customer', + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng( + attraction.latitudeCoordinate, + attraction.longitudeCoordinate, + ), + width: 40.w, + height: 40.h, + child: Icon( + Icons.location_on, + color: Color(0xFFF95F62), + size: 40.sp, + ), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 17.h), + CustomText( + text: attraction.address, + size: 12.sp, + color: Colors.black.withOpacity(0.6), + ), + SizedBox(height: 30.h), + Divider(color: Colors.black.withOpacity(0.2)), + SizedBox(height: 30.h), + Text( + "People frequently ask", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 15.h), + Column( + children: attraction.attractionFaqs.map((faq) { + return Padding( + padding: EdgeInsets.only(bottom: 15.h), + child: faqBox( + title: faq.faqQuestion, + desc: faq.faqAnswer, + ), + ); + }).toList(), + ), + + ], + ), + ), + SizedBox(height: 24.h), + ], + ), + ), + ), + ); + } + + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Text("Something went wrong"), + ), + ); + }, + ), + ); + } + + Widget includedBox(String icon, String title, String disc) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h), + decoration: BoxDecoration( + color: Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(10.r), + border: Border.all(color: Color(0xFFFDCDCE)), + ), + child: Row( + children: [ + Image.asset(icon, scale: 4), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: title, + size: 16.sp, + weight: FontWeight.w500, + color: Color(0xFF212121), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + CustomText( + text: disc, + size: 11.sp, + weight: FontWeight.w400, + color: Color(0xFF666666), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ); + } + + Widget faqBox({ + required String title, + required String desc, + }) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + border: Border.all(color: const Color(0xFFFDCDCE)), + borderRadius: BorderRadius.circular(10.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: CustomText( + text: title, + size: 16.sp, + weight: FontWeight.w500, + color: const Color(0xFF212121), + ), + ), + SizedBox(width: 20.w), + Icon( + Icons.arrow_forward_ios_outlined, + size: 18.sp, + color: Colors.black, + ), + ], + ), + SizedBox(height: 9.h), + CustomText( + text: desc, + size: 11.sp, + color: const Color(0xFF7D7D7D), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/my_pass/views/pass_attractions_page_view.dart b/lib/my_pass/views/pass_attractions_page_view.dart new file mode 100644 index 0000000..a4b427e --- /dev/null +++ b/lib/my_pass/views/pass_attractions_page_view.dart @@ -0,0 +1,160 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/back_widget.dart'; +import 'package:citycards_customer/my_pass/widgets/pass_attraction_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../attractions/blocs/attractions_bloc.dart'; +import '../../attractions/blocs/attractions_event.dart'; +import '../../attractions/blocs/attractions_state.dart'; +import '../../attractions/repository/attractions_repository.dart'; +import '../../attractions/widget/attraction_card.dart'; +import '../../attractions/widget/filter_chip.dart'; +import '../../common_packages/custom_search_field.dart'; + +class PassAttractionsPage extends StatelessWidget { + final String source; + const PassAttractionsPage({super.key, required this.source}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) { + final bloc = AttractionsBloc( + repository: AttractionsRepository(), + ); + + bloc.add( + const FetchAttractionsByCategory(), // No categoryXid parameter + ); + + return bloc; + }, + child: BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // App bar + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + backWidget(context, "Your Attraction", Colors.black), + const SizedBox(height: 20), + + // πŸ” Search field (UI kept, logic disabled) + CommonSearchField( + hint: "Search attractions...", + hintColor: Colors.grey.shade500, + onChanged: (value) { + // ❌ Search logic intentionally disabled + // UI only, no API call + }, + ), + + const SizedBox(height: 16), + + // πŸ–οΈ Category chips row - DYNAMIC + if (state is AttractionsLoaded) + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: state.categories + .map( + (category) => buildCategoryChip( + category.categoryName ?? '', + isSelected: state.selectedCategoryId == category.id, + onTap: () { + bloc.add( + FetchAttractionsByCategory( + categoryXid: category.id, + ), + ); + }, + ), + ) + .toList(), + ), + ), + // else + // // Show placeholder chips while loading + // SingleChildScrollView( + // scrollDirection: Axis.horizontal, + // child: Row( + // children: [ + // buildCategoryChip("Beach", isSelected: true, onTap: () {}), + // buildCategoryChip("Hike", isSelected: false, onTap: () {}), + // buildCategoryChip("Adventure", isSelected: false, onTap: () {}), + // buildCategoryChip("Best in Summer", isSelected: false, onTap: () {}), + // ], + // ), + // ), + + const SizedBox(height: 10), + + // πŸ™οΈ Attraction list + if (state is AttractionsLoading) + const Center( + child: Padding( + padding: EdgeInsets.only(top: 60), + child: CircularProgressIndicator(), + ), + ) + else if (state is AttractionsLoaded) + state.attractions.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: Text( + "No attractions found", + style: TextStyle( + color: Colors.grey, + fontSize: 14.sp, + ), + ), + ), + ) + : Column( + children: state.attractions + .map( + (attraction) => PassAttractionCard( + attraction: attraction, + ), + ) + .toList(), + ) + else if (state is AttractionsError) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: Text( + state.message, + style: TextStyle( + color: Colors.red, + fontSize: 14.sp, + ), + ), + ), + ) + else + const SizedBox(), + ], + ), + ), + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/my_pass/views/pass_details_page_view.dart b/lib/my_pass/views/pass_details_page_view.dart new file mode 100644 index 0000000..e7574a0 --- /dev/null +++ b/lib/my_pass/views/pass_details_page_view.dart @@ -0,0 +1,460 @@ +import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../common_packages/app_bar.dart'; +import '../../common_packages/back_widget.dart'; +import '../../common_packages/custom_dash_border_painter.dart'; +import '../../core/route_constants.dart'; + +class PassDetailsView extends StatelessWidget { + const PassDetailsView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is MyPassLoaded) { + final pass = state.selectedPass!; + + return SafeArea( + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + /// App Bar + SizedBox(height: 10.h), + const CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + + SizedBox(height: 10.h), + backWidget(context, "Back", Colors.black), + + SizedBox(height: 20.h), + + /// ------------------------------- + /// UNLIMITED CARD CONTAINER + /// ------------------------------- + CustomPaint( + painter: DashedBorderPainter( + color: const Color(0xffF95F62), + radius: 20.r, + ), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 18.h), + decoration: BoxDecoration( + color: const Color(0xffF95F62).withOpacity(0.08), + borderRadius: BorderRadius.circular(20.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + /// Title + Text( + pass.title, + style: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: const Color(0xffF95F62), + ), + ), + + SizedBox(height: 18.h), + + /// MAIN CONTENT ROW + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + /// IMAGE + ClipRRect( + borderRadius: BorderRadius.circular(14.r), + child: Image.asset( + "assets/images/unlimited_card_details.png", + height: 100.w, + width: 100.w, + fit: BoxFit.contain, + ), + ), + + SizedBox(width: 14.w), + + /// RIGHT CONTENT + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + /// Adults + Kids (WRAP prevents overflow) + Wrap( + spacing: 10.w, + runSpacing: 10.h, + children: [ + _infoChip( + icon: Icons.person_outline, + text: "Adults-${pass.adults ?? 0}", + ), + _infoChip( + icon: Icons.person_outline, + text: "Kids-${pass.kids ?? 0}", + ), + ], + ), + + SizedBox(height: 12.h), + + /// Days Container (NOT full width) + _infoChip( + icon: Icons.access_time, + text: "${pass.duration} Days", + ), + + SizedBox(height: 14.h), + + /// Valid Till + Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.calendar_today_outlined, + size: 15.sp, + color: const Color(0xffF95F62), + ), + SizedBox(width: 6.w), + + /// "Valid till:" β†’ Black + Text( + "Valid till: ", + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + + /// Date β†’ Red + Text( + pass.validity ?? "", + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: const Color(0xffF95F62), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + SizedBox(height: 24.h), + _sectionTitle("Suggested Attractions"), + SizedBox(height: 12.h), + + _attractionCard(), + SizedBox(height: 12.h), + _attractionCard(), + + SizedBox(height: 16.h), + + _outlineButton( + "View all Attractions", + () { + Navigator.pushNamed( + context, + RouteConstants.passAttractionsPage, + arguments: "qrPass", + ); + }, + ), + + SizedBox(height: 24.h), + + /// ------------------------------- + /// RECOMMENDED OFFERS + /// ------------------------------- + _sectionTitle("Recommended Offers"), + SizedBox(height: 12.h), + + Row( + children: [ + Expanded(child: _offerCard()), + SizedBox(width: 12.w), + Expanded(child: _offerCard()), + ], + ), + + SizedBox(height: 16.h), + + _outlineButton( + "View all Offers", + () { + Navigator.pushNamed( + context, + RouteConstants.searchPassOffer, + ); + }, + ), + + SizedBox(height: 20.h), + + GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.privacyPolicy, // βœ… pass offerId + ); + }, + child: Center( + child: Text( + "Learn about policies", + style: GoogleFonts.poppins( + fontSize: 12.sp, + decoration: TextDecoration.underline, + ), + ), + ), + ), + + SizedBox(height: 30.h), + ], + ), + ), + ), + ); + } + + return const Center(child: CircularProgressIndicator()); + }, + ); + } + + Widget _sectionTitle(String title) { + return Text( + title, + style: GoogleFonts.poppins( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ); + } + + Widget _outlineButton(String title, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 14.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.r), + border: Border.all(color: const Color(0xffF95F62)), + ), + child: Center( + child: Text( + title, + style: GoogleFonts.poppins( + color: const Color(0xffF95F62), + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + } + + Widget _attractionCard() { + return Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: const Color(0xffF2D6D6)), + ), + child: Row( + children: [ + + /// πŸ”₯ Attraction Image (Real Image Style Box) + ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: Image.asset( + "assets/images/aa4.png", // <-- your attraction image + height: 90.w, + width: 90.w, + fit: BoxFit.cover, + ), + ), + + SizedBox(width: 12.w), + + /// πŸ”₯ Text Section + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Koh Rong Samloem", + style: GoogleFonts.poppins( + fontWeight: FontWeight.w600, + fontSize: 14.sp, + ), + ), + + SizedBox(height: 2.h), + + Text( + "Krong Siem Reap", + style: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.grey.shade600, + ), + ), + + SizedBox(height: 4.h), + + Text( + "from \$25/person", + style: GoogleFonts.poppins( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + + SizedBox(height: 6.h), + + Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + "Booking Required", + style: GoogleFonts.poppins( + fontSize: 10.sp, + color: Colors.blue.shade700, + ), + ), + ) + ], + ), + ), + + SizedBox(width: 8.w), + + /// πŸ”₯ QR Code Circle (Proper UI like Design) + Container( + height: 44.w, + width: 44.w, + decoration: BoxDecoration( + color: const Color(0xffF8EDED), // light pink circle bg + shape: BoxShape.circle, + ), + child: Padding( + padding: EdgeInsets.all(10.w), + child: Image.asset( + "assets/images/qr_image.png", + fit: BoxFit.contain, + ), + ), + ), + ], + ), + ); + } + + Widget _infoChip({ + required IconData icon, + required String text, + }) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xffF95F62)), + borderRadius: BorderRadius.circular(14.r), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14.sp, color: const Color(0xffF95F62)), + SizedBox(width: 6.w), + Text( + text, + style: GoogleFonts.poppins( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: const Color(0xffF95F62), + ), + ), + ], + ), + ); + } + + Widget _offerCard() { + return Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: const Color(0xffF2D6D6)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + /// πŸ”₯ Top Offer Image + ClipRRect( + borderRadius: BorderRadius.circular(12.r), + child: Image.asset( + "assets/images/aa4.png", // <-- your offer image + height: 120.h, // πŸ”₯ closer to design ratio + width: double.infinity, + fit: BoxFit.cover, + ), + ), + + SizedBox(height: 12.h), + + /// πŸ”₯ Title + Text( + "Astor Hotels Ultra Deluxe", + style: GoogleFonts.poppins( + fontWeight: FontWeight.w600, + fontSize: 16.sp, + ), + ), + + SizedBox(height: 6.h), + + /// πŸ”₯ Description + Text( + "15% Discount on all treatments for first-time clients", + style: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.grey.shade700, + height: 1.4, + ), + ), + ], + ), + ); + } +} diff --git a/lib/my_pass/views/qr_pass_page_view.dart b/lib/my_pass/views/qr_pass_page_view.dart deleted file mode 100644 index 1ea3a98..0000000 --- a/lib/my_pass/views/qr_pass_page_view.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:citycards_customer/my_pass/blocs/my_pass_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:google_fonts/google_fonts.dart'; - -import '../../common_packages/app_bar.dart'; -import '../../common_packages/back_widget.dart'; -import '../../core/route_constants.dart'; -import '../widgets/action_button_widget.dart'; -import '../widgets/qr_container_widget.dart'; - -class QrPassView extends StatelessWidget { - const QrPassView({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is MyPassLoaded) { - final pass = state.selectedPass!; - return SafeArea( - child: Scaffold( - backgroundColor: Colors.white, - body: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - SizedBox(height: 10.h), - backWidget(context, "Back", Colors.black), - SizedBox(height: 20.h), - SizedBox(height: 10.h), - Text( - "Scan this at the site of\nattraction", - textAlign: TextAlign.center, - style: GoogleFonts.poppins( - fontSize: 13.sp, - color: Colors.black87, - ), - ), - SizedBox(height: 20.h), - - /// ♻️ Reusable QR Container Component - QrContainerWidget( - qrImagePath: "assets/images/qr_image.png", - cityCardTitle: "Melbourne CityCards", - qrCode: "IYFHHVN254ADSD", - cardType: pass.title, - ), - - SizedBox(height: 24.h), - - /// 🎟 Card details section - Container( - padding: EdgeInsets.symmetric( - vertical: 10, - horizontal: 40, - ), - decoration: BoxDecoration( - color: pass.title.toLowerCase() == "unlimited card" - ? const Color(0xffF95F62).withOpacity(0.1) - : const Color(0xffF95FAF).withOpacity(0.1), - borderRadius: BorderRadius.circular(25.r), - border: Border.all( - color: pass.title.toLowerCase() == "unlimited card" - ? const Color(0xffF95F62) - : const Color(0xffF95FAF), - ), - ), - child: Text( - pass.title, - style: GoogleFonts.poppins( - fontSize: 16.sp, - color: const Color(0xffFF5A5F), - fontWeight: FontWeight.w500, - ), - ), - ), - SizedBox(height: 6.h), - Text( - "Adults-${pass.adults} β€’ Kids-${pass.kids} β€’ ${pass.duration}", - style: GoogleFonts.poppins( - fontSize: 12.sp, - color: Color(0xff212121), - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 4.h), - Text( - "Valid Till: ${pass.validity}", - style: GoogleFonts.poppins( - fontSize: 12.sp, - color: Color(0xff212121), - fontWeight: FontWeight.w400, - ), - ), - - SizedBox(height: 28.h), - Align( - alignment: Alignment.centerLeft, - child: Text( - "Learn about policies", - style: GoogleFonts.poppins( - color: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.w500, - decoration: TextDecoration.underline, - ), - ), - ), - SizedBox(height: 24.h), - - /// πŸ”˜ Buttons - Column( - children: [ - actionButton( - label: "View All Attractions", - onPressed: () { - Navigator.of(context).pushNamed(RouteConstants.attractionsPage, arguments: "qrPass"); - }, - ), - SizedBox(height: 12.h), - actionButton( - label: "View All Available Offers", - onPressed: () { - Navigator.of(context).pushNamed(RouteConstants.searchOffer); - }, - ), - ], - ), - ], - ), - ), - ), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); - } -} diff --git a/lib/my_pass/views/search_pass_offers_with_listing.dart b/lib/my_pass/views/search_pass_offers_with_listing.dart new file mode 100644 index 0000000..37cedd9 --- /dev/null +++ b/lib/my_pass/views/search_pass_offers_with_listing.dart @@ -0,0 +1,348 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/custom_search_field.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/core/route_constants.dart'; +import 'package:citycards_customer/search_offers/bloc/offers_bloc.dart'; +import 'package:citycards_customer/search_offers/bloc/offers_event.dart'; +import 'package:citycards_customer/search_offers/bloc/offers_state.dart'; +import 'package:citycards_customer/search_offers/repository/offers_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../common_packages/common_app_texts.dart'; +import '../../networkApiServices/api_urls.dart'; + +class PassOffersScreen extends StatefulWidget { + const PassOffersScreen({super.key}); + + @override + State createState() => _PassOffersScreenState(); +} + +class _PassOffersScreenState extends State { + int? selectedCategoryId; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => OffersBloc(OffersRepository())..add(LoadOffers()), + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showCart: false, + showDivider: true, + ), + Row( + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Icon(Icons.arrow_back), + ), + SizedBox(width: 8.w), + CustomText( + text: "Offers with ${CommonAppText.selectiveCard} Card", + size: 12.sp, + ), + ], + ), + SizedBox(height: 33.h), + Builder( + builder: (context) => CommonSearchField( + hint: "Search offers", + hintColor: const Color(0xFFF95F62).withOpacity(.6), + showSuffix: true, + onChanged: (value) { + context.read().add(SearchOffers(value)); + }, + ), + ), + SizedBox(height: 20.h), + + /// Dynamic Categories + BlocBuilder( + builder: (context, state) { + if (state is OffersLoaded) { + final categories = state.categories; + + if (categories.isEmpty) { + return SizedBox.shrink(); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...List.generate(categories.length, (index) { + final category = categories[index]; + final isSelected = + selectedCategoryId == category.id; + + return Padding( + padding: EdgeInsets.only(right: 8.0.w), + child: GestureDetector( + onTap: () { + setState(() { + if (selectedCategoryId == category.id) { + // Deselect if already selected + selectedCategoryId = null; + context + .read() + .add(LoadOffers()); + } else { + // Select new category + selectedCategoryId = category.id; + context.read().add( + LoadOffers( + categoryXid: category.id), + ); + } + }); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, + ), + decoration: BoxDecoration( + color: isSelected + ? Color(0xFFF95F62) + : Color(0xFFFEE7E7), + borderRadius: + BorderRadius.circular(100.sp), + border: Border.all( + color: isSelected + ? Color(0xFFF95F62) + : Color(0xFFFDCDCE), + ), + ), + child: Center( + child: CustomText( + text: category.categoryName, + color: isSelected + ? Colors.white + : Color(0xFFF95F62), + ), + ), + ), + ), + ); + }), + ], + ), + ); + } + + return SizedBox.shrink(); + }, + ), + SizedBox(height: 20.h), + + /// Offer list + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is OffersLoading) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xFFF95F62), + ), + ); + } + + if (state is OffersError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48.sp, + color: Colors.red, + ), + SizedBox(height: 16.h), + Text( + "Error: ${state.message}", + style: TextStyle( + color: Colors.red, + fontSize: 14.sp, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + if (state is OffersLoaded) { + final offers = state.offers; + + if (offers.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.local_offer_outlined, + size: 48.sp, + color: Colors.grey, + ), + SizedBox(height: 16.h), + Text( + "No offers found", + style: TextStyle( + color: Colors.grey, + fontSize: 14.sp, + ), + ), + ], + ), + ); + } + + return GridView.builder( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16.w, + mainAxisSpacing: 22.h, + childAspectRatio: 0.65, + ), + itemCount: offers.length, + itemBuilder: (context, index) { + final offer = offers[index]; + return InkWell( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.offerPassDetail, + arguments: offer.id, // βœ… pass offerId + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, + vertical: 6.h, + ), + decoration: BoxDecoration( + border: Border.all( + color: + Color(0xFFF95F62).withOpacity(.24), + ), + borderRadius: BorderRadius.circular(12.sp), + ), + child: Column( + children: [ + ClipRRect( + borderRadius: + BorderRadius.circular(8.sp), + child: offer.mobileBannerImage != null && + offer.mobileBannerImage! + .isNotEmpty + ? Image.network( + '${ApiUrls.baseUrl}/${offer.mobileBannerImage}', + width: double.infinity, + height: 120.5.h, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) { + return Container( + width: double.infinity, + height: 120.5.h, + color: Color(0xFFFEE7E7), + child: Icon( + Icons.local_offer, + size: 40.sp, + color: Color(0xFFF95F62) + .withOpacity(.6), + ), + ); + }, + loadingBuilder: (context, child, + loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Container( + width: double.infinity, + height: 120.5.h, + color: Color(0xFFFEE7E7), + child: Center( + child: + CircularProgressIndicator( + value: loadingProgress + .expectedTotalBytes != + null + ? loadingProgress + .cumulativeBytesLoaded / + loadingProgress + .expectedTotalBytes! + : null, + strokeWidth: 2, + color: + Color(0xFFF95F62), + ), + ), + ); + }, + ) + : Container( + width: double.infinity, + height: 120.5.h, + color: Color(0xFFFEE7E7), + child: Icon( + Icons.local_offer, + size: 40.sp, + color: Color(0xFFF95F62) + .withOpacity(.6), + ), + ), + ), + SizedBox(height: 8.h), + CustomText( + text: offer.title, + size: 18.sp, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 8.h), + CustomText( + text: offer.description, + color: Colors.black.withOpacity(.6), + size: 12.sp, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + }, + ); + } + + return const Center( + child: Text( + "No data available", + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/my_pass/widgets/pass_attraction_card.dart b/lib/my_pass/widgets/pass_attraction_card.dart new file mode 100644 index 0000000..6519c40 --- /dev/null +++ b/lib/my_pass/widgets/pass_attraction_card.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../attractions/models/attraction_model.dart'; +import '../../common_packages/common_app_texts.dart'; +import '../../core/route_constants.dart'; + +class PassAttractionCard extends StatelessWidget { + final Attraction attraction; + const PassAttractionCard({super.key, required this.attraction}); + + @override + Widget build(BuildContext context) { + /// CARD TITLES (instead of categories) + final List tags = attraction.cards + .map((e) => e.title) + .where((e) => e.isNotEmpty) + .toList(); + + /// GALLERY IMAGE (handled safely in model) + final String imageUrl = attraction.coverImageUrl; + + return InkWell( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.passAttractionDetails, + arguments: attraction.id, + ); + }, + child: Container( + margin: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.w), + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(15.r), + color: const Color(0xffFFF5F5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// IMAGE (network with fallback) + ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: imageUrl.isNotEmpty + ? Image.network( + imageUrl, + height: 94.h, + width: 94.w, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _imageFallback(), + ) + : _imageFallback(), + ), + + SizedBox(width: 10.w), + + /// CONTENT + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + attraction.title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + + SizedBox(height: 6.h), + + Text( + attraction.address, + style: GoogleFonts.poppins( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff464646), + ), + ), + + SizedBox(height: 6.h), + + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "from \$${attraction.ticketPriceAdult}", + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + TextSpan( + text: "/person", + style: TextStyle( + fontSize: 10.sp, + color: Colors.black, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + + SizedBox(height: 6.h), + + /// TAGS (CARD TITLES) + attraction.isBookingRequired == false + ? Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: tags + .map( + (tag) => Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: tag == + "${CommonAppText.selectiveCard} Card" + ? const Color(0xffF95FAF) + .withOpacity(0.1) + : const Color(0xffF95F62) + .withOpacity(0.1), + border: Border.all( + color: tag == + "${CommonAppText.selectiveCard} Card" + ? const Color(0xffF95FAF) + : const Color(0xffF95F62), + ), + borderRadius: + BorderRadius.circular(20.r), + ), + child: Text( + tag, + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: const Color(0xff1A1A1A), + fontWeight: FontWeight.w400, + ), + ), + ), + ) + .toList(), + ) + : Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, + ), + decoration: BoxDecoration( + color: const Color(0xffC1D2F8), + border: Border.all( + color: const Color(0xff2563EB), + ), + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + "Booking Required", + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: const Color(0xff1A1A1A), + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// SAME PLACEHOLDER AS BEFORE + Widget _imageFallback() { + return Container( + height: 94.h, + width: 94.w, + color: Colors.grey.shade200, + child: Icon( + Icons.image_not_supported_outlined, + size: 28.sp, + color: Colors.grey, + ), + ); + } +} diff --git a/lib/profile/bloc/profile/profile_bloc.dart b/lib/profile/bloc/profile/profile_bloc.dart index 4a3a6e3..afc961e 100644 --- a/lib/profile/bloc/profile/profile_bloc.dart +++ b/lib/profile/bloc/profile/profile_bloc.dart @@ -26,7 +26,6 @@ class ProfileBloc extends Bloc { Emitter emit, ) async { try { - emit(const ProfileLoading()); final profile = await _profileRepository.fetchUserProfile(); @@ -54,6 +53,12 @@ class ProfileBloc extends Bloc { print('πŸ“„ [BLOC] Address1: ${event.address1}'); print('πŸ“„ [BLOC] Address2: ${event.address2}'); + // ⭐ NEW DEBUG LOGS + print('πŸ“„ [BLOC] City: ${event.city}'); + print('πŸ“„ [BLOC] State: ${event.state}'); + print('πŸ“„ [BLOC] Country: ${event.country}'); + print('πŸ“„ [BLOC] Postal Code: ${event.postalCode}'); + if (event.profileImageFile != null) { print('πŸ“„ [BLOC] βœ… Profile Image File Present in Event'); print('πŸ“„ [BLOC] File Path: ${event.profileImageFile!.path}'); diff --git a/lib/profile/bloc/profile/profile_event.dart b/lib/profile/bloc/profile/profile_event.dart index 3ec20c4..bd10d29 100644 --- a/lib/profile/bloc/profile/profile_event.dart +++ b/lib/profile/bloc/profile/profile_event.dart @@ -18,6 +18,7 @@ class FetchProfileEvent extends ProfileEvent { List get props => [userId]; } +/// Event to update user profile /// Event to update user profile class UpdateProfileEvent extends ProfileEvent { final int userId; @@ -26,6 +27,10 @@ class UpdateProfileEvent extends ProfileEvent { final String mobileNumber; final String? address1; final String? address2; + final String? city; // ⭐ NEW + final String? state; // ⭐ NEW + final String? country; // ⭐ NEW + final String? postalCode; // ⭐ NEW final File? profileImageFile; const UpdateProfileEvent({ @@ -35,6 +40,10 @@ class UpdateProfileEvent extends ProfileEvent { required this.mobileNumber, this.address1, this.address2, + this.city, // ⭐ NEW + this.state, // ⭐ NEW + this.country, // ⭐ NEW + this.postalCode, // ⭐ NEW this.profileImageFile, }); @@ -46,6 +55,10 @@ class UpdateProfileEvent extends ProfileEvent { mobileNumber, address1, address2, + city, // ⭐ NEW + state, // ⭐ NEW + country, // ⭐ NEW + postalCode, // ⭐ NEW profileImageFile, ]; @@ -56,6 +69,10 @@ class UpdateProfileEvent extends ProfileEvent { 'mobileNumber': mobileNumber, if (address1 != null && address1!.isNotEmpty) 'address1': address1, if (address2 != null && address2!.isNotEmpty) 'address2': address2, + if (city != null && city!.isNotEmpty) 'city': city, // ⭐ NEW + if (state != null && state!.isNotEmpty) 'state': state, // ⭐ NEW + if (country != null && country!.isNotEmpty) 'country': country, // ⭐ NEW + if (postalCode != null && postalCode!.isNotEmpty) 'postalCode': postalCode, // ⭐ NEW }; } } diff --git a/lib/profile/repository/profile_repository.dart b/lib/profile/repository/profile_repository.dart index cbd035e..bf9cb48 100644 --- a/lib/profile/repository/profile_repository.dart +++ b/lib/profile/repository/profile_repository.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; + import '../models/profile_model.dart'; import '../../networkApiServices/network_api_services.dart'; import '../../networkApiServices/api_urls.dart'; @@ -9,7 +10,7 @@ import '../../localPreference/local_preference.dart'; class ProfileRepository { final NetworkApiService _apiService = NetworkApiService(); - /// Fetch user profile (userId from local storage) + /// βœ… Fetch user profile (userId from local storage) Future fetchUserProfile() async { final int? userId = await LocalPreference.getUserId(); @@ -20,11 +21,10 @@ class ProfileRepository { return ProfileModel.fromJson(response.data); } - /// Update user profile (userId from local storage) - /// ⭐ FIXED: Now uses multipart/form-data for file upload + /// βœ… Update user profile (Multipart with Image + New Address Fields) Future updateUserProfile({ required Map data, - File? profileImageFile, // ⭐ NEW: Accept File instead of base64 + File? profileImageFile, }) async { final int? userId = await LocalPreference.getUserId(); @@ -32,31 +32,56 @@ class ProfileRepository { print('πŸ“€ [UPDATE PROFILE] User ID: $userId'); print('πŸ“€ [UPDATE PROFILE] URL: ${ApiUrls.userProfile}/$userId'); print('πŸ“€ [UPDATE PROFILE] Data Keys: ${data.keys.toList()}'); - print('πŸ“€ [UPDATE PROFILE] First Name: ${data['firstName']}'); - print('πŸ“€ [UPDATE PROFILE] Last Name: ${data['lastName']}'); - print('πŸ“€ [UPDATE PROFILE] Mobile: ${data['mobileNumber']}'); - print('πŸ“€ [UPDATE PROFILE] Address1: ${data['address1']}'); - print('πŸ“€ [UPDATE PROFILE] Address2: ${data['address2']}'); - print('πŸ“€ [UPDATE PROFILE] Profile Image File: ${profileImageFile?.path}'); + + print('πŸ“€ First Name: ${data['firstName']}'); + print('πŸ“€ Last Name: ${data['lastName']}'); + print('πŸ“€ Mobile: ${data['mobileNumber']}'); + print('πŸ“€ Address1: ${data['address1']}'); + print('πŸ“€ Address2: ${data['address2']}'); + + // ⭐ NEW DEBUG LOGS + print('πŸ“€ City: ${data['city']}'); + print('πŸ“€ State: ${data['state']}'); + print('πŸ“€ Country: ${data['country']}'); + print('πŸ“€ Postal Code: ${data['postalCode']}'); + + print('πŸ“€ Profile Image File: ${profileImageFile?.path}'); } - // ⭐ Create FormData for multipart/form-data upload + /// βœ… Create FormData (Multipart) final formData = FormData(); - // Add text fields + /// βœ… Add Text Fields formData.fields.addAll([ MapEntry('firstName', data['firstName']), MapEntry('lastName', data['lastName']), MapEntry('mobileNumber', data['mobileNumber']), + if (data['address1'] != null && data['address1'].toString().isNotEmpty) MapEntry('address1', data['address1']), + if (data['address2'] != null && data['address2'].toString().isNotEmpty) MapEntry('address2', data['address2']), + + /// ⭐ NEW FIELDS + if (data['city'] != null && data['city'].toString().isNotEmpty) + MapEntry('city', data['city']), + + if (data['state'] != null && data['state'].toString().isNotEmpty) + MapEntry('state', data['state']), + + if (data['country'] != null && data['country'].toString().isNotEmpty) + MapEntry('country', data['country']), + + if (data['postalCode'] != null && + data['postalCode'].toString().isNotEmpty) + MapEntry('postalCode', data['postalCode']), ]); - // ⭐ Add profile image file if provided + /// βœ… Add Profile Image File if (profileImageFile != null) { final fileName = profileImageFile.path.split('/').last; + formData.files.add( MapEntry( 'profileImage', @@ -68,48 +93,38 @@ class ProfileRepository { ); if (kDebugMode) { - print('πŸ“€ [UPDATE PROFILE] βœ… Profile Image File Added'); - print('πŸ“€ [UPDATE PROFILE] File Name: $fileName'); - print('πŸ“€ [UPDATE PROFILE] File Path: ${profileImageFile.path}'); final fileSize = await profileImageFile.length(); - print('πŸ“€ [UPDATE PROFILE] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); + print('πŸ“€ βœ… Profile Image Added'); + print('πŸ“€ File Name: $fileName'); + print( + 'πŸ“€ File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); } } else { if (kDebugMode) { - print('πŸ“€ [UPDATE PROFILE] ⚠️ No profile image file provided'); + print('πŸ“€ ⚠️ No profile image provided'); } } - // ⭐ Send as multipart/form-data + /// βœ… API Call (Multipart PUT) final response = await _apiService.putApi( url: '${ApiUrls.userProfile}/$userId', data: formData, ); if (kDebugMode) { - print('πŸ“€ [UPDATE PROFILE] βœ… Response Status: Success'); - print('πŸ“€ [UPDATE PROFILE] Full Response: ${response.data}'); - - // Check if response has nested 'user' object - if (response.data.containsKey('user')) { - print('πŸ“€ [UPDATE PROFILE] βœ… Response has nested "user" object'); - print('πŸ“€ [UPDATE PROFILE] User Data: ${response.data['user']}'); - print('πŸ“€ [UPDATE PROFILE] Updated Profile Image: ${response.data['user']['profileImage']}'); - } else { - print('πŸ“€ [UPDATE PROFILE] Response structure: ${response.data.keys.toList()}'); - print('πŸ“€ [UPDATE PROFILE] Updated Profile Image: ${response.data['profileImage']}'); - } + print('πŸ“€ βœ… Response Success'); + print('πŸ“€ Full Response: ${response.data}'); } - // Extract user data from nested response + /// βœ… Handle Nested Response (user object) final userData = response.data.containsKey('user') ? response.data['user'] : response.data; if (kDebugMode) { - print('πŸ“€ [UPDATE PROFILE] Parsing ProfileModel from: $userData'); + print('πŸ“€ Parsing ProfileModel from: $userData'); } return ProfileModel.fromJson(userData); } -} \ No newline at end of file +} diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart index b58ca4c..718d451 100644 --- a/lib/profile/view/edit_profile/edit_profile_view.dart +++ b/lib/profile/view/edit_profile/edit_profile_view.dart @@ -30,6 +30,10 @@ class _EditProfilePageState extends State { final TextEditingController phoneController = TextEditingController(); final TextEditingController address1Controller = TextEditingController(); final TextEditingController address2Controller = TextEditingController(); + final TextEditingController stateController = TextEditingController(); + final TextEditingController countryController = TextEditingController(); + final TextEditingController cityController = TextEditingController(); + final TextEditingController zipCodeController = TextEditingController(); final _formKey = GlobalKey(); final ImagePicker _picker = ImagePicker(); @@ -64,6 +68,10 @@ class _EditProfilePageState extends State { phoneController.text = profile.mobileNumber; address1Controller.text = profile.address1 ?? ''; address2Controller.text = profile.address2 ?? ''; + stateController.text = profile.stateName ?? ''; + countryController.text = profile.country ?? ''; + cityController.text = profile.cityName ?? ''; + zipCodeController.text = profile.zipCode ?? ''; // ⭐ REMOVED setState - image is now managed by BLoC state if (kDebugMode && profile.profileImage != null && profile.profileImage!.isNotEmpty) { @@ -321,6 +329,19 @@ class _EditProfilePageState extends State { address2: address2Controller.text.trim().isEmpty ? null : address2Controller.text.trim(), + // ⭐ ADD THESE NEW FIELDS + city: cityController.text.trim().isEmpty + ? null + : cityController.text.trim(), + state: stateController.text.trim().isEmpty + ? null + : stateController.text.trim(), + country: countryController.text.trim().isEmpty + ? null + : countryController.text.trim(), + postalCode: zipCodeController.text.trim().isEmpty + ? null + : zipCodeController.text.trim(), profileImageFile: imageFileToSend, ), ); @@ -333,6 +354,10 @@ class _EditProfilePageState extends State { phoneController.dispose(); address1Controller.dispose(); address2Controller.dispose(); + stateController.dispose(); + countryController.dispose(); + cityController.dispose(); + zipCodeController.dispose(); super.dispose(); } @@ -495,7 +520,7 @@ class _EditProfilePageState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.0.w), child: CustomTextField( - label: "Address 1", + label: "Address", hint: "Enter address manually or tap to search", controller: address1Controller, enabled: !isLoading, @@ -512,6 +537,46 @@ class _EditProfilePageState extends State { ), ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0.w), + child: CustomTextField( + label: "State", + hint: "Select your State", + controller: stateController, + enabled: !isLoading, + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0.w), + child: CustomTextField( + label: "Country", + hint: "Select your Country", + controller: countryController, + enabled: !isLoading, + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0.w), + child: CustomTextField( + label: "City", + hint: "Enter the name of your city", + controller: cityController, + enabled: !isLoading, + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0.w), + child: CustomTextField( + label: "ZIP Code", + hint: "Enter the ZIP code you reside in", + controller: zipCodeController, + enabled: !isLoading, + ), + ), + SizedBox(height: 26.h), // Buttons diff --git a/lib/search_offers/view/search_offers_with_listing.dart b/lib/search_offers/view/search_offers_with_listing.dart index 059201b..f6bfb88 100644 --- a/lib/search_offers/view/search_offers_with_listing.dart +++ b/lib/search_offers/view/search_offers_with_listing.dart @@ -221,12 +221,12 @@ class _OffersScreenState extends State { itemBuilder: (context, index) { final offer = offers[index]; return InkWell( - onTap: () { - Navigator.of(context).pushNamed( - RouteConstants.offerPassDetail, - arguments: offer.id, // βœ… pass offerId - ); - }, + // onTap: () { + // Navigator.of(context).pushNamed( + // RouteConstants.offerPassDetail, + // arguments: offer.id, // βœ… pass offerId + // ); + // }, child: Container( padding: EdgeInsets.symmetric( horizontal: 6.w,