From 09726eb4e69d4bdeed63bbee922fabeb14f0f9af Mon Sep 17 00:00:00 2001 From: Shreeyash Thorat <120039092+ShreeyashThorat@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:34:34 +0530 Subject: [PATCH 1/8] API Integration --- .../view/create_account_view.dart | 21 +- .../bloc/get_itinerary_bloc.dart | 20 ++ .../bloc/get_itinerary_cities_bloc.dart | 26 ++ .../bloc/get_itinerary_cities_event.dart | 10 + .../bloc/get_itinerary_cities_state.dart | 22 ++ .../bloc/get_itinerary_event.dart | 10 + .../bloc/get_itinerary_state.dart | 19 ++ .../bloc/itinerary_detail_bloc.dart | 42 +-- .../models/current_location_model.dart | 6 + .../models/itinerary_city_model.dart | 57 ++++ .../repository/itinerary_repository.dart | 36 +++ .../city_selection_view.dart | 255 +++++++----------- .../current_location_selection.dart | 102 +++++-- .../date_selection_view.dart | 58 ++-- .../itinerary_completion_view.dart | 2 +- .../views/itinerary_creation_view.dart | 7 +- .../views/magic_itinerary_view.dart | 117 ++++---- lib/networkApiServices/api_urls.dart | 7 +- .../network_api_services.dart | 14 +- pubspec.lock | 86 +++++- pubspec.yaml | 3 + 21 files changed, 597 insertions(+), 323 deletions(-) create mode 100644 lib/itinerary_creation/bloc/get_itinerary_bloc.dart create mode 100644 lib/itinerary_creation/bloc/get_itinerary_cities_bloc.dart create mode 100644 lib/itinerary_creation/bloc/get_itinerary_cities_event.dart create mode 100644 lib/itinerary_creation/bloc/get_itinerary_cities_state.dart create mode 100644 lib/itinerary_creation/bloc/get_itinerary_event.dart create mode 100644 lib/itinerary_creation/bloc/get_itinerary_state.dart create mode 100644 lib/itinerary_creation/models/current_location_model.dart create mode 100644 lib/itinerary_creation/models/itinerary_city_model.dart create mode 100644 lib/itinerary_creation/repository/itinerary_repository.dart diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart index a0d3c5e..4bea299 100644 --- a/lib/create_account/view/create_account_view.dart +++ b/lib/create_account/view/create_account_view.dart @@ -15,7 +15,7 @@ import '../repository/create_account_repository.dart'; class CreateAccountView extends StatelessWidget { final String email; - CreateAccountView({super.key,required this.email}); + CreateAccountView({super.key, required this.email}); final TextEditingController firstNameController = TextEditingController(); final TextEditingController lastNameController = TextEditingController(); @@ -29,9 +29,9 @@ class CreateAccountView extends StatelessWidget { emailController.text.trim().isEmpty || phoneController.text.trim().isEmpty || addressController.text.trim().isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please fill all fields')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Please fill all fields'))); return; } @@ -51,15 +51,14 @@ class CreateAccountView extends StatelessWidget { Widget build(BuildContext context) { emailController.text = email; return BlocProvider( - create: (context) => CreateAccountBloc( - repository: CreateAccountRepository(), - ), + create: (context) => + CreateAccountBloc(repository: CreateAccountRepository()), child: BlocListener( - listener: (context, state) async { + listener: (ctx, state) async { if (state is CreateAccountSuccess) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.message))); await LocalPreference.setLogin(true); final userId = await LocalPreference.getUserId(); context.read().add(FetchProfileEvent(userId: userId!)); diff --git a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart new file mode 100644 index 0000000..ffbca46 --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart @@ -0,0 +1,20 @@ +import 'package:bloc/bloc.dart'; +import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart'; +import 'package:equatable/equatable.dart'; + +part 'get_itinerary_event.dart'; +part 'get_itinerary_state.dart'; + +class GetItineraryBloc extends Bloc { + GetItineraryBloc() : super(GetItineraryInitial()) { + on((event, emit) { + try { + emit(GetItineraryLoading()); + final data = ItineraryRepository().fetchItinerary(); + emit(GetItinerarySuccessfully()); + } catch (e) { + emit(GetItineraryFailed(error: "Something went wrong")); + } + }); + } +} diff --git a/lib/itinerary_creation/bloc/get_itinerary_cities_bloc.dart b/lib/itinerary_creation/bloc/get_itinerary_cities_bloc.dart new file mode 100644 index 0000000..e8d6582 --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_cities_bloc.dart @@ -0,0 +1,26 @@ +import 'dart:developer'; + +import 'package:bloc/bloc.dart'; +import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart'; +import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart'; +import 'package:equatable/equatable.dart'; + +part 'get_itinerary_cities_event.dart'; +part 'get_itinerary_cities_state.dart'; + +class GetItineraryCitiesBloc + extends Bloc { + GetItineraryCitiesBloc() : super(GetItineraryCitiesInitial()) { + on((event, emit) async { + try { + log("Getting cities"); + emit(GetItineraryCitiesLoading()); + final data = await ItineraryRepository().fetchItineraryCities(); + emit(GetItineraryCitiesSuccessfully(cities: data)); + } catch (e) { + log("Fetch Itierary - ${e.toString()}"); + emit(GetItineraryCitiesFailed(error: "Something went wrong")); + } + }); + } +} diff --git a/lib/itinerary_creation/bloc/get_itinerary_cities_event.dart b/lib/itinerary_creation/bloc/get_itinerary_cities_event.dart new file mode 100644 index 0000000..3c3628d --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_cities_event.dart @@ -0,0 +1,10 @@ +part of 'get_itinerary_cities_bloc.dart'; + +abstract class GetItineraryCitiesEvent extends Equatable { + const GetItineraryCitiesEvent(); + + @override + List get props => []; +} + +class GetItineraryCities extends GetItineraryCitiesEvent {} diff --git a/lib/itinerary_creation/bloc/get_itinerary_cities_state.dart b/lib/itinerary_creation/bloc/get_itinerary_cities_state.dart new file mode 100644 index 0000000..5328a26 --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_cities_state.dart @@ -0,0 +1,22 @@ +part of 'get_itinerary_cities_bloc.dart'; + +abstract class GetItineraryCitiesState extends Equatable { + const GetItineraryCitiesState(); + + @override + List get props => []; +} + +class GetItineraryCitiesInitial extends GetItineraryCitiesState {} + +class GetItineraryCitiesLoading extends GetItineraryCitiesState {} + +class GetItineraryCitiesSuccessfully extends GetItineraryCitiesState { + final List cities; + const GetItineraryCitiesSuccessfully({required this.cities}); +} + +class GetItineraryCitiesFailed extends GetItineraryCitiesState { + final String error; + const GetItineraryCitiesFailed({required this.error}); +} diff --git a/lib/itinerary_creation/bloc/get_itinerary_event.dart b/lib/itinerary_creation/bloc/get_itinerary_event.dart new file mode 100644 index 0000000..a774fef --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_event.dart @@ -0,0 +1,10 @@ +part of 'get_itinerary_bloc.dart'; + +abstract class GetItineraryEvent extends Equatable { + const GetItineraryEvent(); + + @override + List get props => []; +} + +class GetIiterary extends GetItineraryEvent {} diff --git a/lib/itinerary_creation/bloc/get_itinerary_state.dart b/lib/itinerary_creation/bloc/get_itinerary_state.dart new file mode 100644 index 0000000..035989d --- /dev/null +++ b/lib/itinerary_creation/bloc/get_itinerary_state.dart @@ -0,0 +1,19 @@ +part of 'get_itinerary_bloc.dart'; + +abstract class GetItineraryState extends Equatable { + const GetItineraryState(); + + @override + List get props => []; +} + +final class GetItineraryInitial extends GetItineraryState {} + +class GetItineraryLoading extends GetItineraryState {} + +class GetItinerarySuccessfully extends GetItineraryState {} + +class GetItineraryFailed extends GetItineraryState { + final String error; + const GetItineraryFailed({required this.error}); +} diff --git a/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart b/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart index 82aebaa..3beefd2 100644 --- a/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart +++ b/lib/itinerary_creation/bloc/itinerary_detail_bloc.dart @@ -1,6 +1,9 @@ +import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; +import '../models/current_location_model.dart'; + abstract class ItineraryDetailEvent {} class AddDateToItinerary extends ItineraryDetailEvent { @@ -10,11 +13,17 @@ class AddDateToItinerary extends ItineraryDetailEvent { } class AddCityToItinerary extends ItineraryDetailEvent { - final String city; + final ItineraryCityModel city; AddCityToItinerary(this.city); } +class AddAddressToItinerary extends ItineraryDetailEvent { + final CurrentLocationModel address; + + AddAddressToItinerary(this.address); +} + class AddEnergyToItinerary extends ItineraryDetailEvent { final String energy; @@ -65,7 +74,7 @@ class AddShoppingRating extends ItineraryDetailEvent { class ItineraryDetailState { final String? selectedDate; - final String? selectedCity; + final ItineraryCityModel? selectedCity; final String? selectedEnergy; final String? withKid; final String? selectedDietary; @@ -74,6 +83,7 @@ class ItineraryDetailState { final String? culturalRating; final String? wildLifeRating; final String? shoppingRating; + final CurrentLocationModel? baseAdd; ItineraryDetailState({ this.selectedDate, @@ -86,19 +96,21 @@ class ItineraryDetailState { this.culturalRating, this.wildLifeRating, this.shoppingRating, + this.baseAdd, }); ItineraryDetailState copyWith({ String? selectedDate, - String? selectedCity, + ItineraryCityModel? selectedCity, String? selectedEnergy, String? withKid, - String? selectedDietary, + String? selectedDietary, String? museumRating, String? scenicRating, String? culturalRating, String? wildLifeRating, String? shoppingRating, + CurrentLocationModel? baseAdd, }) { return ItineraryDetailState( selectedDate: selectedDate ?? this.selectedDate, @@ -111,6 +123,7 @@ class ItineraryDetailState { culturalRating: culturalRating ?? this.culturalRating, wildLifeRating: wildLifeRating ?? this.wildLifeRating, shoppingRating: shoppingRating ?? this.shoppingRating, + baseAdd: baseAdd ?? this.baseAdd, ); } } @@ -121,15 +134,6 @@ class AddItineraryDetailBloc : super( ItineraryDetailState( selectedDate: DateFormat('EEEE, MMMM d, yyyy').format(DateTime.now()), - selectedCity: "Paris", - selectedEnergy: "", - withKid: "", - selectedDietary: "", - museumRating: "", - scenicRating: "", - culturalRating: "", - wildLifeRating: "", - shoppingRating: "", ), ) { on((event, emit) { @@ -137,10 +141,13 @@ class AddItineraryDetailBloc }); on((event, emit) { - print("Selected city: ${event.city}"); emit(state.copyWith(selectedCity: event.city)); }); + on((event, emit) { + emit(state.copyWith(baseAdd: event.address)); + }); + on((event, emit) { emit(state.copyWith(selectedEnergy: event.energy)); }); @@ -150,13 +157,6 @@ class AddItineraryDetailBloc }); on((event, emit) { - // final currentSelection = List.from(state.selectedDietary ?? []); - // - // if (currentSelection.contains(event.dietary)) { - // currentSelection.remove(event.dietary); - // } else { - // currentSelection.add(event.dietary); - // } emit(state.copyWith(selectedDietary: event.dietary)); }); diff --git a/lib/itinerary_creation/models/current_location_model.dart b/lib/itinerary_creation/models/current_location_model.dart new file mode 100644 index 0000000..ca7222b --- /dev/null +++ b/lib/itinerary_creation/models/current_location_model.dart @@ -0,0 +1,6 @@ +class CurrentLocationModel { + final String? baseAdd; + final double? lat; + final double? lan; + CurrentLocationModel({this.baseAdd, this.lan, this.lat}); +} diff --git a/lib/itinerary_creation/models/itinerary_city_model.dart b/lib/itinerary_creation/models/itinerary_city_model.dart new file mode 100644 index 0000000..e3ac99d --- /dev/null +++ b/lib/itinerary_creation/models/itinerary_city_model.dart @@ -0,0 +1,57 @@ +class ItineraryCityModel { + int? id; + String? cityName; + String? urlSlug; + int? iconXid; + Icon? icon; + + ItineraryCityModel({ + this.id, + this.cityName, + this.urlSlug, + this.iconXid, + this.icon, + }); + + ItineraryCityModel.fromJson(Map json) { + id = json['id']; + cityName = json['cityName']; + urlSlug = json['urlSlug']; + iconXid = json['iconXid']; + icon = json['icon'] != null ? Icon.fromJson(json['icon']) : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['cityName'] = cityName; + data['urlSlug'] = urlSlug; + data['iconXid'] = iconXid; + if (icon != null) { + data['icon'] = icon!.toJson(); + } + return data; + } +} + +class Icon { + int? id; + String? iconName; + String? iconSvg; + + Icon({this.id, this.iconName, this.iconSvg}); + + Icon.fromJson(Map json) { + id = json['id']; + iconName = json['iconName']; + iconSvg = json['iconSvg']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['iconName'] = iconName; + data['iconSvg'] = iconSvg; + return data; + } +} diff --git a/lib/itinerary_creation/repository/itinerary_repository.dart b/lib/itinerary_creation/repository/itinerary_repository.dart new file mode 100644 index 0000000..ddff6a1 --- /dev/null +++ b/lib/itinerary_creation/repository/itinerary_repository.dart @@ -0,0 +1,36 @@ +import 'dart:developer'; + +import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart'; +import 'package:dio/dio.dart'; + +import '../../networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class ItineraryRepository { + final NetworkApiService _apiService = NetworkApiService(); + + Future fetchItinerary() async { + final response = await _apiService.getApi(url: ApiUrls.getItinerary); + + return response.data; + } + + Future> fetchItineraryCities() async { + try { + final response = await _apiService.getApi( + url: ApiUrls.getItineraryCities, + ); + final List cities = (response.data as List) + .map((e) => ItineraryCityModel.fromJson(e as Map)) + .toList(); + + return cities; + } on DioException catch (e) { + // log("Error logged - ${e.response}"); + throw e.response!.data["message"] ?? "Something went wrong"; + } catch (e, stack) { + log("Error logged - ${stack.toString()}"); + rethrow; + } + } +} diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart index be65f52..6705792 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/city_selection_view.dart @@ -1,47 +1,30 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart'; -import 'package:citycards_customer/common_packages/custom_search_field.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:citycards_customer/common_packages/custom_textfield.dart'; +import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_cities_bloc.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_detail_bloc.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; +import 'package:citycards_customer/networkApiServices/api_urls.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -class MenuItem { - final int id; - final String label; - final String flag; +class CitySelectionView extends StatefulWidget { + const CitySelectionView({super.key}); - MenuItem(this.id, this.label, this.flag); + @override + State createState() => _CitySelectionViewState(); } -List menuItems = [ - MenuItem(1, 'Paris', "🇫🇷"), - MenuItem(2, 'Tokyo', "🇯🇵"), - MenuItem(3, 'New York', "🇺🇸"), - MenuItem(4, 'London', "🇬🇧"), - MenuItem(5, 'Barcelona', "🇪🇸"), - MenuItem(6, 'Dubai', "🇦🇪"), - MenuItem(7, 'Rome', "🇮🇹"), - MenuItem(8, 'Bangkok', "🇹🇭"), -]; - -class CitySelectionView extends StatelessWidget { - CitySelectionView({super.key}); - - final List> cityList = [ - {"flag": "🇫🇷", "city": "Paris"}, - {"flag": "🇯🇵", "city": "Tokyo"}, - {"flag": "🇺🇸", "city": "New York"}, - {"flag": "🇬🇧", "city": "London"}, - {"flag": "🇪🇸", "city": "Barcelona"}, - {"flag": "🇦🇪", "city": "Dubai"}, - {"flag": "🇮🇹", "city": "Rome"}, - {"flag": "🇹🇭", "city": "Bangkok"}, - ]; - +class _CitySelectionViewState extends State { final TextEditingController cityController = TextEditingController(); + final GetItineraryCitiesBloc getItineraryCitiesBloc = + GetItineraryCitiesBloc(); + @override + void initState() { + getItineraryCitiesBloc.add(GetItineraryCities()); + super.initState(); + } @override Widget build(BuildContext context) { @@ -60,89 +43,6 @@ class CitySelectionView extends StatelessWidget { ), SizedBox(height: 32.h), - Container( - height: 56.h, - padding: EdgeInsets.only(left: 20.w), - decoration: BoxDecoration( - border: Border.all(color: Color(0xFFF95F62)), - borderRadius: BorderRadius.circular(28), - ), - child: Row( - children: [ - Image.asset("assets/icons/location.png", scale: 4), - Expanded( - child: SizedBox( - child: - BlocBuilder< - AddItineraryDetailBloc, - ItineraryDetailState - >( - builder: (context, state) { - final selectedMenuItem = menuItems.firstWhere( - (menu) => menu.label == state.selectedCity, - orElse: () => - menuItems.first, // fallback if not found - ); - return DropdownMenu( - controller: cityController, - initialSelection: selectedMenuItem, - width: double.infinity, - hintText: "Select City", - requestFocusOnTap: true, - enableFilter: true, - showTrailingIcon: false, - onSelected: (MenuItem? menu) { - context.read().add( - AddCityToItinerary(menu!.label), - ); - }, - inputDecorationTheme: InputDecorationTheme( - contentPadding: EdgeInsets.symmetric( - vertical: 6.h, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(28), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(28), - borderSide: const BorderSide( - color: Colors.transparent, - ), - ), - ), - - menuStyle: MenuStyle( - backgroundColor: WidgetStateProperty.all( - Colors.white, - ), - maximumSize: WidgetStateProperty.all( - Size.infinite, - ), - ), - dropdownMenuEntries: menuItems - .map>(( - MenuItem menu, - ) { - return DropdownMenuEntry( - value: menu, - label: menu.label, - leadingIcon: CustomText(text: menu.flag), - ); - }) - .toList(), - ); - }, - ), - ), - ), - ], - ), - ), - SizedBox(height: 16.h), Align( alignment: Alignment.topLeft, @@ -154,57 +54,86 @@ class CitySelectionView extends StatelessWidget { ), ), SizedBox(height: 10.h), - SizedBox( - height: 175.h, - child: BlocBuilder( - builder: (context, state) { - return GridView.builder( - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - mainAxisSpacing: 16.h, - crossAxisSpacing: 16.w, - ), - itemCount: cityList.length, - itemBuilder: (context, index) { - final item = cityList[index]; - final isSelected = item['city'] == state.selectedCity; - return GestureDetector( - onTap: () { - context.read().add( - AddCityToItinerary(item['city'] ?? ""), - ); - }, - child: Container( - height: 78.h, - width: 76.w, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? Color(0xFFF95F62) - : Colors.transparent, - ), + BlocBuilder( + bloc: getItineraryCitiesBloc, + builder: (ctx, state1) { + if (state1 is GetItineraryCitiesLoading) { + return Center(child: CircularProgressIndicator()); + } else if (state1 is GetItineraryCitiesFailed) { + return Center(child: Text(state1.error)); + } else if (state1 is GetItineraryCitiesSuccessfully && + state1.cities.isEmpty) { + return Center(child: Text("Data not found")); + } else if (state1 is GetItineraryCitiesSuccessfully) { + return SizedBox( + height: 175.h, + child: BlocBuilder( + builder: (context, state) { + return GridView.builder( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 16.h, + crossAxisSpacing: 16.w, ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CustomText(text: item['flag'] ?? ""), - SizedBox(height: 4.h), - CustomText( - text: item['city'] ?? "", - size: 12.sp, - color: Color(0xFF364153), + itemCount: state1.cities.length, + itemBuilder: (context, index) { + final item = state1.cities[index]; + final isSelected = item == state.selectedCity; + return GestureDetector( + onTap: () { + context.read().add( + AddCityToItinerary(item), + ); + }, + child: Container( + height: 78.h, + width: 76.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? Color(0xFFF95F62) + : Colors.transparent, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CachedNetworkImage( + imageUrl: + "${ApiUrls.baseUrl}${item.icon!.iconSvg!}", + width: 20, + height: 20, + fit: BoxFit.cover, + placeholder: (context, url) => Container( + width: 20, + height: 20, + color: Colors.grey.shade50, + ), + errorWidget: (context, url, error) => + const Icon(Icons.flag, size: 20), + ), + SizedBox(height: 4.h), + CustomText( + text: item.cityName ?? "", + size: 12.sp, + color: Color(0xFF364153), + ), + ], + ), ), - ], - ), - ), - ); - }, + ); + }, + ); + }, + ), ); - }, - ), + } + + return Container(); + }, ), SizedBox(height: 40.h), CustomFilledButton( diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart b/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart index 6280e50..e4e685d 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart @@ -1,11 +1,16 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; +import 'package:citycards_customer/itinerary_creation/models/current_location_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; +import 'package:latlong2/latlong.dart'; + +import '../../bloc/itinerary_detail_bloc.dart'; class CurrentLocationSelection extends StatefulWidget { const CurrentLocationSelection({super.key}); @@ -21,6 +26,7 @@ class _CurrentLocationSelectionState extends State { Future _getCurrentLocation() async { LocationPermission permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { ScaffoldMessenger.of(context).showSnackBar( @@ -33,11 +39,40 @@ class _CurrentLocationSelectionState extends State { desiredAccuracy: LocationAccuracy.high, ); + final lat = position.latitude; + final lng = position.longitude; + setState(() { - _currentLatLng = LatLng(position.latitude, position.longitude); - _controller.text = - "Lat: ${position.latitude.toStringAsFixed(5)}, Lng: ${position.longitude.toStringAsFixed(5)}"; + _currentLatLng = LatLng(lat, lng); }); + + await _getAddressFromLatLng(lat, lng); + } + + Future _getAddressFromLatLng(double lat, double lng) async { + try { + final placemarks = await placemarkFromCoordinates(lat, lng); + + if (placemarks.isNotEmpty) { + final place = placemarks.first; + + final address = [ + place.name, + place.street, + place.subLocality, + place.locality, + place.administrativeArea, + place.postalCode, + place.country, + ].where((e) => e != null && e.isNotEmpty).join(', '); + + setState(() { + _controller.text = address; + }); + } + } catch (e) { + debugPrint("Reverse geocoding error: $e"); + } } @override @@ -98,32 +133,38 @@ class _CurrentLocationSelectionState extends State { child: SizedBox( height: 250.h, width: double.infinity, - - child: Image.asset( - "assets/images/attra_detail_map.png", - fit: BoxFit.cover, - height: 236.h, + child: FlutterMap( + options: MapOptions( + initialCenter: _currentLatLng!, + initialZoom: 15, + ), + children: [ + TileLayer( + urlTemplate: + "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + subdomains: const ['a', 'b', 'c'], + userAgentPackageName: 'com.citycards.customer', + ), + MarkerLayer( + markers: [ + Marker( + point: _currentLatLng!, + width: 40, + height: 40, + child: const Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + ], ), - // child: GoogleMap( - // initialCameraPosition: CameraPosition( - // target: _currentLatLng!, - // zoom: 15, - // ), - // markers: { - // Marker( - // markerId: const MarkerId("currentLocation"), - // position: _currentLatLng!, - // ), - // }, - // myLocationEnabled: true, - // myLocationButtonEnabled: false, - // ), ), ) : GestureDetector( - onTap: () { - _getCurrentLocation(); - }, + onTap: _getCurrentLocation, child: Container( height: 46.h, padding: EdgeInsets.symmetric(horizontal: 12.w), @@ -155,6 +196,15 @@ class _CurrentLocationSelectionState extends State { // --- Continue button --- CustomFilledButton( onTap: () { + context.read().add( + AddAddressToItinerary( + CurrentLocationModel( + baseAdd: _controller.text, + lan: _currentLatLng?.latitude, + lat: _currentLatLng?.latitude, + ), + ), + ); context.read().add( ItineraryStepNavigationNextEvent(), ); diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart index 7fec6df..39031fb 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/date_selection_view.dart @@ -27,35 +27,35 @@ class DateSelectionView extends StatelessWidget { ), SizedBox(height: 32.h), - Container( - height: 90.h, - padding: EdgeInsets.symmetric(horizontal: 20.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(28), - border: Border.all(color: Color(0xFFF95F62), width: 1.1.w), - ), - child: Row( - children: [ - GestureDetector( - onTap: () { - _pickDate(context); - }, - child: Image.asset("assets/icons/calender.png", scale: 4), - ), - SizedBox(width: 16.w), - BlocBuilder( - builder: (context, state) { - return CustomText( - text: state.selectedDate ?? "", - size: 14.sp, - color: Color(0xFF101828), - ); - }, - ), - const Spacer(), - Icon(Icons.check_circle, color: Color(0xFFF95F62)), - ], + GestureDetector( + onTap: () { + _pickDate(context); + }, + child: Container( + height: 90.h, + padding: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(28), + border: Border.all(color: Color(0xFFF95F62), width: 1.1.w), + ), + child: Row( + children: [ + Image.asset("assets/icons/calender.png", scale: 4), + SizedBox(width: 16.w), + BlocBuilder( + builder: (context, state) { + return CustomText( + text: state.selectedDate ?? "", + size: 14.sp, + color: Color(0xFF101828), + ); + }, + ), + const Spacer(), + Icon(Icons.check_circle, color: Color(0xFFF95F62)), + ], + ), ), ), SizedBox(height: 32.h), diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart index cbec5be..20ce599 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart @@ -69,7 +69,7 @@ class ItineraryCompletionView extends StatelessWidget { ), _buildProfileRow( "City", - state.selectedCity ?? "", + state.selectedCity!.cityName ?? "", ), _buildProfileRow( "Energy", diff --git a/lib/itinerary_creation/views/itinerary_creation_view.dart b/lib/itinerary_creation/views/itinerary_creation_view.dart index 75bd61d..01f9438 100644 --- a/lib/itinerary_creation/views/itinerary_creation_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_view.dart @@ -1,4 +1,4 @@ - +import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_cities_bloc.dart'; import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart'; import 'package:flutter/material.dart'; @@ -105,7 +105,10 @@ class _ItineraryCreationPageState extends State { children: [ DateSelectionView(), CurrentLocationSelection(), - CitySelectionView(), + BlocProvider( + create: (context) => GetItineraryCitiesBloc(), + child: CitySelectionView(), + ), EnergySelectionView(), KidsSelectionView(), DietarySelectionView(), diff --git a/lib/itinerary_creation/views/magic_itinerary_view.dart b/lib/itinerary_creation/views/magic_itinerary_view.dart index 95f492f..83ed7cb 100644 --- a/lib/itinerary_creation/views/magic_itinerary_view.dart +++ b/lib/itinerary_creation/views/magic_itinerary_view.dart @@ -43,63 +43,63 @@ class _MagicItineraryViewState extends State { child: isLoading ? Center(child: CircularProgressIndicator()) : Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: SingleChildScrollView( - child: Column( - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showDivider: false, - ), - SizedBox(height: 24.h), + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: SingleChildScrollView( + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: false, + ), + SizedBox(height: 24.h), - // Show different UI based on login status - if (isLoggedIn) ...[ - ItineraryFilledCard(), - SizedBox(height: 32.h), - CustomPaint( - painter: DottedBorderPainter(), - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 24.h), - decoration: BoxDecoration( - color: Color(0xFFF95F62).withOpacity(0.25), - borderRadius: BorderRadius.circular(12.sp), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CustomText( - text: "Plan your next adventure", - color: Color(0xFF656565), - size: 14.sp, - ), - SizedBox(height: 16.h), - CustomFilledButton( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - ItineraryCreationStartPage(), + // Show different UI based on login status + if (isLoggedIn) ...[ + ItineraryFilledCard(), + SizedBox(height: 32.h), + CustomPaint( + painter: DottedBorderPainter(), + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: Color(0xFFF95F62).withOpacity(0.25), + borderRadius: BorderRadius.circular(12.sp), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomText( + text: "Plan your next adventure", + color: Color(0xFF656565), + size: 14.sp, ), - ); - }, - label: "Create My Itinerary", - showArrow: true, + SizedBox(height: 16.h), + CustomFilledButton( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ItineraryCreationStartPage(), + ), + ); + }, + label: "Create My Itinerary", + showArrow: true, + ), + ], + ), ), - ], - ), - ), + ), + ] else ...[ + EmptyItineraryView(), + ], + ], ), - ] else ...[ - EmptyItineraryView(), - ], - ], - ), - ), - ), + ), + ), ), ); } @@ -151,9 +151,7 @@ class EmptyItineraryView extends StatelessWidget { context: context, isScrollControlled: true, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)), ), builder: (_) => const LoginEmailBottomsheet(), ); @@ -247,8 +245,9 @@ class ItineraryFilledCard extends StatelessWidget { SizedBox(height: 12.h), InkWell( onTap: () { - Navigator.of(context) - .pushReplacementNamed(RouteConstants.yourItinerary); + Navigator.of( + context, + ).pushReplacementNamed(RouteConstants.yourItinerary); }, child: Container( height: 43.h, @@ -270,4 +269,4 @@ class ItineraryFilledCard extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 3e10e99..079ae14 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -1,5 +1,4 @@ class ApiUrls { - static const baseUrl = "https://devapi.citycards.betadelivery.com"; static const refreshToken = "$baseUrl/auth/refresh"; @@ -16,7 +15,9 @@ class ApiUrls { static const buyAPass = "$baseUrl/mobile/pass"; static const offersDetails = "$baseUrl/mobile/list/offers"; static const myPostCards = "$baseUrl/mobile/postcards/all"; - + static const getItinerary = "$baseUrl/mobile/itinerary/all-initineraries"; + static const getItineraryCities = + "$baseUrl/mobile/itinerary/cities-with-icons"; //Post Apis static const createAccount = "$baseUrl/mobile/user/register"; @@ -24,4 +25,4 @@ class ApiUrls { static const verifyOtp = "$baseUrl/mobile/user/verify-otp"; static const submitTicket = "$baseUrl/mobile/user/support"; static const createPostCard = "$baseUrl/mobile/postcards"; -} \ No newline at end of file +} diff --git a/lib/networkApiServices/network_api_services.dart b/lib/networkApiServices/network_api_services.dart index 78f626e..072d976 100644 --- a/lib/networkApiServices/network_api_services.dart +++ b/lib/networkApiServices/network_api_services.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../localPreference/local_preference.dart'; @@ -34,14 +36,17 @@ class NetworkApiService { const maxRetries = 2; final currentRetry = options.extra['retry'] as int? ?? 0; - final shouldRetry = currentRetry < maxRetries && + final shouldRetry = + currentRetry < maxRetries && (err.type == DioExceptionType.connectionTimeout || err.type == DioExceptionType.sendTimeout || err.type == DioExceptionType.receiveTimeout); if (shouldRetry) { if (kDebugMode) { - print('🔁 Retrying request (${currentRetry + 1}) => ${options.uri}'); + print( + '🔁 Retrying request (${currentRetry + 1}) => ${options.uri}', + ); } options.extra['retry'] = currentRetry + 1; @@ -65,6 +70,7 @@ class NetworkApiService { QueuedInterceptorsWrapper( onRequest: (options, handler) async { final token = await LocalPreference.getAccessToken(); + if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } @@ -188,9 +194,7 @@ class NetworkApiService { final response = await _dio.post( ApiUrls.refreshToken, data: {"refreshToken": refreshToken}, - options: Options( - headers: {'Authorization': null}, - ), + options: Options(headers: {'Authorization': null}), ); await LocalPreference.setAccessToken(response.data['accessToken']); diff --git a/pubspec.lock b/pubspec.lock index ff9f06d..c3d6afb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -34,13 +34,13 @@ packages: source: hosted version: "2.13.0" bloc: - dependency: transitive + dependency: "direct main" description: name: bloc - sha256: e18b8e7825e9921d67a6d256dba0b6015ece8a577eb0a411845c46a352994d78 + sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81 url: "https://pub.dev" source: hosted - version: "9.0.1" + version: "9.2.0" boolean_selector: dependency: transitive description: @@ -49,6 +49,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -262,6 +286,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_glass_morphism: dependency: "direct main" description: @@ -365,6 +397,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.1" + geocoding: + dependency: "direct main" + description: + name: geocoding + sha256: "606be036287842d779d7ec4e2f6c9435fc29bbbd3c6da6589710f981d8852895" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geocoding_android: + dependency: transitive + description: + name: geocoding_android + sha256: ba810da90d6633cbb82bbab630e5b4a3b7d23503263c00ae7f1ef0316dcae5b9 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + geocoding_ios: + dependency: transitive + description: + name: geocoding_ios + sha256: "18ab1c8369e2b0dcb3a8ccc907319334f35ee8cf4cfef4d9c8e23b13c65cb825" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + geocoding_platform_interface: + dependency: transitive + description: + name: geocoding_platform_interface + sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b" + url: "https://pub.dev" + source: hosted + version: "3.2.0" geolocator: dependency: "direct main" description: @@ -709,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" opentype_dart: dependency: transitive description: @@ -837,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sanitize_html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 655b564..b87fc0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,9 @@ dependencies: sqflite: ^2.4.2 flutter_map: ^8.2.2 flutter_stripe: ^12.2.0 + geocoding: ^4.0.0 + cached_network_image: ^3.4.1 + bloc: ^9.2.0 dev_dependencies: flutter_test: From 0c663bdec7322a604225a7767f4dc684a52747dc Mon Sep 17 00:00:00 2001 From: mystery012728 Date: Tue, 10 Feb 2026 10:44:19 +0530 Subject: [PATCH 2/8] pull taken of shreeyash and conflict solved --- assets/images/no_itinerary.png | Bin 0 -> 94264 bytes .../bloc/stripe_payment_bloc.dart | 136 +++- .../bloc/stripe_payment_event.dart | 20 +- .../bloc/stripe_payment_state.dart | 71 +- lib/StripePayment/view/stripe_payment.dart | 614 ++++++++++++------ lib/checkout/bloc/checkOut/checkout_bloc.dart | 71 +- .../bloc/checkOut/checkout_event.dart | 16 +- .../bloc/checkOut/checkout_state.dart | 10 + lib/checkout/view/checkout_view.dart | 277 ++------ .../bloc/get_itinerary_bloc.dart | 68 +- .../bloc/get_itinerary_event.dart | 2 + .../bloc/get_itinerary_state.dart | 17 +- .../models/my_itinerary_model.dart | 240 +++++++ .../repository/itinerary_repository.dart | 10 +- .../views/magic_itinerary_view.dart | 368 ++++++++--- lib/localPreference/local_database.dart | 8 + lib/localPreference/local_preference.dart | 26 + lib/login/view/verify_otp_bottomsheet.dart | 6 +- lib/main.dart | 6 + lib/networkApiServices/api_urls.dart | 5 +- .../blocs/postcard_creation_bloc.dart | 3 +- .../blocs/postcard_creation_events.dart | 2 + .../blocs/postcard_creation_state.dart | 6 +- .../views/my_postcard_preview_view.dart | 81 +-- .../views/order_success_page_view.dart | 19 +- .../views/postcard_checkout_page_view.dart | 46 +- .../postcard_purchase_form_page_view.dart | 73 +-- .../preview_postcard_step_page_view.dart | 20 +- lib/postcard/widgets/back_card_widget.dart | 53 +- lib/postcard/widgets/front_card_widget.dart | 74 ++- 30 files changed, 1630 insertions(+), 718 deletions(-) create mode 100644 assets/images/no_itinerary.png create mode 100644 lib/itinerary_creation/models/my_itinerary_model.dart diff --git a/assets/images/no_itinerary.png b/assets/images/no_itinerary.png new file mode 100644 index 0000000000000000000000000000000000000000..031c98fc9fa7361f82e21f56ee4949ece8afd515 GIT binary patch literal 94264 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 G*#800wU!J3 literal 0 HcmV?d00001 diff --git a/lib/StripePayment/bloc/stripe_payment_bloc.dart b/lib/StripePayment/bloc/stripe_payment_bloc.dart index 4b63321..9509b3d 100644 --- a/lib/StripePayment/bloc/stripe_payment_bloc.dart +++ b/lib/StripePayment/bloc/stripe_payment_bloc.dart @@ -15,7 +15,9 @@ class StripePaymentBloc extends Bloc { super(const StripePaymentInitial()) { on(_onInitiatePayment); on(_onInitiatePaymentWithClientSecret); + on(_onCancelPayment); on(_onResetPaymentState); + on(_onRetryPayment); } Future _onInitiatePayment( @@ -23,7 +25,9 @@ class StripePaymentBloc extends Bloc { Emitter emit, ) async { try { - emit(const StripePaymentLoading()); + emit(const StripePaymentLoading( + message: 'Creating payment intent...', + )); /// Stripe expects smallest currency unit /// USD → cents, INR → paise @@ -35,6 +39,10 @@ class StripePaymentBloc extends Bloc { currency: event.currency, ); + emit(const StripePaymentLoading( + message: 'Initializing payment sheet...', + )); + // 2️⃣ Init Payment Sheet await Stripe.instance.initPaymentSheet( paymentSheetParameters: SetupPaymentSheetParameters( @@ -44,36 +52,36 @@ class StripePaymentBloc extends Bloc { ), ); + emit(const StripePaymentSheetReady()); + + emit(const StripePaymentLoading( + message: 'Processing payment...', + )); + // 3️⃣ Show Payment Sheet await Stripe.instance.presentPaymentSheet(); // ✅ SUCCESS emit(const StripePaymentSuccess()); } on StripeException catch (e) { - // Handle Stripe-specific errors - if (e.error.code == FailureCode.Canceled) { - emit(StripePaymentCancelled( - message: e.error.localizedMessage ?? 'Payment Cancelled', - )); - } else { - emit(StripePaymentFailure( - error: e.error.localizedMessage ?? 'Payment failed', - )); - } + _handleStripeException(e, emit); } catch (e) { emit(StripePaymentFailure( - error: e.toString(), + error: 'An unexpected error occurred: ${e.toString()}', + isRetryable: true, )); } } - /// 🆕 NEW: Handle payment with clientSecret directly from backend + /// Handle payment with clientSecret directly from backend Future _onInitiatePaymentWithClientSecret( InitiatePaymentWithClientSecret event, Emitter emit, ) async { try { - emit(const StripePaymentLoading()); + emit(const StripePaymentLoading( + message: 'Initializing payment...', + )); // 1️⃣ Init Payment Sheet with clientSecret from backend await Stripe.instance.initPaymentSheet( @@ -84,33 +92,111 @@ class StripePaymentBloc extends Bloc { ), ); + emit(const StripePaymentSheetReady()); + + emit(const StripePaymentLoading( + message: 'Processing payment...', + )); + // 2️⃣ Show Payment Sheet await Stripe.instance.presentPaymentSheet(); // ✅ SUCCESS emit(const StripePaymentSuccess()); } on StripeException catch (e) { - // Handle Stripe-specific errors - if (e.error.code == FailureCode.Canceled) { - emit(StripePaymentCancelled( - message: e.error.localizedMessage ?? 'Payment Cancelled', - )); - } else { - emit(StripePaymentFailure( - error: e.error.localizedMessage ?? 'Payment failed', - )); - } + _handleStripeException(e, emit); } catch (e) { emit(StripePaymentFailure( - error: e.toString(), + error: 'An unexpected error occurred: ${e.toString()}', + isRetryable: true, )); } } + /// Handle payment cancellation + void _onCancelPayment( + CancelPaymentEvent event, + Emitter emit, + ) { + emit(const StripePaymentCancelled( + message: 'Payment cancelled by user', + )); + } + + /// Handle payment retry + Future _onRetryPayment( + RetryPaymentEvent event, + Emitter emit, + ) async { + // Reset state first + emit(const StripePaymentInitial()); + + // Then initiate payment again + add(InitiatePaymentWithClientSecret( + clientSecret: event.clientSecret, + )); + } + + /// Reset payment state back to initial void _onResetPaymentState( ResetPaymentState event, Emitter emit, ) { emit(const StripePaymentInitial()); } + + /// Centralized Stripe exception handling + void _handleStripeException( + StripeException e, + Emitter emit, + ) { + final errorCode = e.error.code; + final errorMessage = e.error.localizedMessage ?? 'Payment failed'; + + // Handle cancellation separately + if (errorCode == FailureCode.Canceled) { + emit(StripePaymentCancelled( + message: errorMessage, + )); + return; + } + + // Handle different error types + switch (errorCode) { + case FailureCode.Failed: + emit(StripePaymentFailure( + error: errorMessage, + errorCode: errorCode.toString(), + isRetryable: true, + )); + break; + + case FailureCode.Timeout: + emit(const StripePaymentFailure( + error: 'Payment timed out. Please try again.', + errorCode: 'timeout', + isRetryable: true, + )); + break; + + default: + emit(StripePaymentFailure( + error: errorMessage, + errorCode: errorCode?.toString(), + isRetryable: _isRetryableError(errorCode), + )); + } + } + + /// Determine if an error is retryable + bool _isRetryableError(FailureCode? errorCode) { + if (errorCode == null) return true; + + // Non-retryable errors + const nonRetryableErrors = [ + // Add specific non-retryable error codes here if needed + ]; + + return !nonRetryableErrors.contains(errorCode); + } } \ No newline at end of file diff --git a/lib/StripePayment/bloc/stripe_payment_event.dart b/lib/StripePayment/bloc/stripe_payment_event.dart index 470e359..ecd1f8d 100644 --- a/lib/StripePayment/bloc/stripe_payment_event.dart +++ b/lib/StripePayment/bloc/stripe_payment_event.dart @@ -20,7 +20,7 @@ class InitiatePayment extends StripePaymentEvent { List get props => [amount, currency]; } -/// 🆕 NEW: Event to initiate payment with clientSecret from backend +/// Event to initiate payment with clientSecret from backend class InitiatePaymentWithClientSecret extends StripePaymentEvent { final String clientSecret; @@ -32,6 +32,24 @@ class InitiatePaymentWithClientSecret extends StripePaymentEvent { List get props => [clientSecret]; } +/// Event to cancel ongoing payment +class CancelPaymentEvent extends StripePaymentEvent { + const CancelPaymentEvent(); +} + +/// Event to reset payment state back to initial class ResetPaymentState extends StripePaymentEvent { const ResetPaymentState(); +} + +/// Event to retry failed payment +class RetryPaymentEvent extends StripePaymentEvent { + final String clientSecret; + + const RetryPaymentEvent({ + required this.clientSecret, + }); + + @override + List get props => [clientSecret]; } \ No newline at end of file diff --git a/lib/StripePayment/bloc/stripe_payment_state.dart b/lib/StripePayment/bloc/stripe_payment_state.dart index 1d6383c..98d4ba5 100644 --- a/lib/StripePayment/bloc/stripe_payment_state.dart +++ b/lib/StripePayment/bloc/stripe_payment_state.dart @@ -7,36 +7,59 @@ abstract class StripePaymentState extends Equatable { List get props => []; } +/// Initial state before any payment action class StripePaymentInitial extends StripePaymentState { const StripePaymentInitial(); } +/// Payment is being processed class StripePaymentLoading extends StripePaymentState { - const StripePaymentLoading(); -} + final String? message; -class StripePaymentSuccess extends StripePaymentState { - final String message; - - const StripePaymentSuccess({ - this.message = 'Payment Successful', + const StripePaymentLoading({ + this.message, }); @override List get props => [message]; } -class StripePaymentFailure extends StripePaymentState { - final String error; +/// Payment sheet is initialized and ready to be presented +class StripePaymentSheetReady extends StripePaymentState { + const StripePaymentSheetReady(); +} - const StripePaymentFailure({ - required this.error, +/// Payment was successful +class StripePaymentSuccess extends StripePaymentState { + final String message; + final String? paymentIntentId; + + const StripePaymentSuccess({ + this.message = 'Payment Successful', + this.paymentIntentId, }); @override - List get props => [error]; + List get props => [message, paymentIntentId]; } +/// Payment failed +class StripePaymentFailure extends StripePaymentState { + final String error; + final String? errorCode; + final bool isRetryable; + + const StripePaymentFailure({ + required this.error, + this.errorCode, + this.isRetryable = true, + }); + + @override + List get props => [error, errorCode, isRetryable]; +} + +/// Payment was cancelled by user class StripePaymentCancelled extends StripePaymentState { final String message; @@ -44,6 +67,30 @@ class StripePaymentCancelled extends StripePaymentState { this.message = 'Payment Cancelled', }); + @override + List get props => [message]; +} + +/// Payment requires additional authentication (3D Secure, etc.) +class StripePaymentRequiresAction extends StripePaymentState { + final String message; + + const StripePaymentRequiresAction({ + this.message = 'Additional authentication required', + }); + + @override + List get props => [message]; +} + +/// Payment is processing on the backend +class StripePaymentProcessing extends StripePaymentState { + final String message; + + const StripePaymentProcessing({ + this.message = 'Payment is being processed...', + }); + @override List get props => [message]; } \ No newline at end of file diff --git a/lib/StripePayment/view/stripe_payment.dart b/lib/StripePayment/view/stripe_payment.dart index 8bd1d47..25536e0 100644 --- a/lib/StripePayment/view/stripe_payment.dart +++ b/lib/StripePayment/view/stripe_payment.dart @@ -1,230 +1,456 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + import '../bloc/stripe_payment_bloc.dart'; import '../bloc/stripe_payment_event.dart'; import '../bloc/stripe_payment_state.dart'; import '../repository/stripe_service.dart'; -class StripePaymentView extends StatelessWidget { - const StripePaymentView({super.key}); +/// 🎯 Reusable Stripe Payment Screen +/// +/// This widget handles Stripe payment flow and can be used across different features +/// like postcards, subscriptions, bookings, etc. +class StripePaymentScreen extends StatelessWidget { + /// Client secret from your backend payment intent + final String clientSecret; - @override - Widget build(BuildContext context) { - final args = - ModalRoute.of(context)!.settings.arguments as Map; + /// Amount to display (optional) + final double? amount; - final double amount = args['amount']; - final String currency = args['currency']; + /// Currency symbol (default: \$) + final String currencySymbol; - return BlocProvider( - create: (context) => StripePaymentBloc( - stripeService: StripeService(), - ), - child: StripePaymentViewContent( - amount: amount, - currency: currency, - ), - ); - } -} + /// Custom title for the payment screen + final String? title; -class StripePaymentViewContent extends StatefulWidget { - final double amount; - final String currency; + /// Custom loading message + final String loadingMessage; - const StripePaymentViewContent({ + /// Custom success message + final String successMessage; + + /// Custom failure message prefix + final String failureMessage; + + /// Callback when payment succeeds + final VoidCallback? onPaymentSuccess; + + /// Callback when payment fails + final void Function(String error)? onPaymentFailure; + + /// Callback when payment is cancelled + final VoidCallback? onPaymentCancelled; + + /// Primary color for the UI + final Color primaryColor; + + /// Success icon color + final Color successColor; + + /// Error icon color + final Color errorColor; + + /// Custom height ratio (0.0 to 1.0) + final double heightRatio; + + /// Whether to show close button during loading + final bool showCloseButtonDuringLoading; + + /// Custom widget to show above the status (optional) + final Widget? headerWidget; + + /// Custom widget to show below the status (optional) + final Widget? footerWidget; + + const StripePaymentScreen({ super.key, - required this.amount, - required this.currency, + required this.clientSecret, + this.amount, + this.currencySymbol = '\$', + this.title, + this.loadingMessage = 'Processing payment...', + this.successMessage = 'Payment Successful!', + this.failureMessage = 'Payment Failed', + this.onPaymentSuccess, + this.onPaymentFailure, + this.onPaymentCancelled, + this.primaryColor = const Color(0xFFF95F62), + this.successColor = Colors.green, + this.errorColor = Colors.red, + this.heightRatio = 0.5, + this.showCloseButtonDuringLoading = false, + this.headerWidget, + this.footerWidget, }); - @override - State createState() => - _StripePaymentViewContentState(); -} + /// 🚀 Static method to show as bottom sheet + static Future showAsBottomSheet({ + required BuildContext context, + required String clientSecret, + double? amount, + String currencySymbol = '\$', + String? title, + String loadingMessage = 'Processing payment...', + String successMessage = 'Payment Successful!', + String failureMessage = 'Payment Failed', + VoidCallback? onPaymentSuccess, + void Function(String error)? onPaymentFailure, + VoidCallback? onPaymentCancelled, + Color primaryColor = const Color(0xFFF95F62), + Color successColor = Colors.green, + Color errorColor = Colors.red, + double heightRatio = 0.5, + bool isDismissible = false, + bool enableDrag = false, + bool showCloseButtonDuringLoading = false, + Widget? headerWidget, + Widget? footerWidget, + }) async { + return await showModalBottomSheet( + context: context, + isDismissible: isDismissible, + enableDrag: enableDrag, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (bottomSheetContext) { + return BlocProvider( + create: (_) => StripePaymentBloc(stripeService: StripeService()) + ..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)), + child: StripePaymentScreen( + clientSecret: clientSecret, + amount: amount, + currencySymbol: currencySymbol, + title: title, + loadingMessage: loadingMessage, + successMessage: successMessage, + failureMessage: failureMessage, + onPaymentSuccess: onPaymentSuccess, + onPaymentFailure: onPaymentFailure, + onPaymentCancelled: onPaymentCancelled, + primaryColor: primaryColor, + successColor: successColor, + errorColor: errorColor, + heightRatio: heightRatio, + showCloseButtonDuringLoading: showCloseButtonDuringLoading, + headerWidget: headerWidget, + footerWidget: footerWidget, + ), + ); + }, + ); + } -class _StripePaymentViewContentState extends State { - @override - void initState() { - super.initState(); - // Automatically initiate payment when screen loads - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().add( - InitiatePayment( - amount: widget.amount, - currency: widget.currency, - ), - ); - }); + /// 🚀 Static method to show as full screen dialog + static Future showAsDialog({ + required BuildContext context, + required String clientSecret, + double? amount, + String currencySymbol = '\$', + String? title, + String loadingMessage = 'Processing payment...', + String successMessage = 'Payment Successful!', + String failureMessage = 'Payment Failed', + VoidCallback? onPaymentSuccess, + void Function(String error)? onPaymentFailure, + VoidCallback? onPaymentCancelled, + Color primaryColor = const Color(0xFFF95F62), + Color successColor = Colors.green, + Color errorColor = Colors.red, + bool barrierDismissible = false, + bool showCloseButtonDuringLoading = false, + Widget? headerWidget, + Widget? footerWidget, + }) async { + return await showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (dialogContext) { + return BlocProvider( + create: (_) => StripePaymentBloc(stripeService: StripeService()) + ..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)), + child: Dialog( + backgroundColor: Colors.transparent, + child: StripePaymentScreen( + clientSecret: clientSecret, + amount: amount, + currencySymbol: currencySymbol, + title: title, + loadingMessage: loadingMessage, + successMessage: successMessage, + failureMessage: failureMessage, + onPaymentSuccess: onPaymentSuccess, + onPaymentFailure: onPaymentFailure, + onPaymentCancelled: onPaymentCancelled, + primaryColor: primaryColor, + successColor: successColor, + errorColor: errorColor, + heightRatio: 1.0, + showCloseButtonDuringLoading: showCloseButtonDuringLoading, + headerWidget: headerWidget, + footerWidget: footerWidget, + ), + ), + ); + }, + ); } @override Widget build(BuildContext context) { - return BlocListener( + return BlocConsumer( listener: (context, state) { if (state is StripePaymentSuccess) { - // Show success message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.green, - duration: const Duration(seconds: 2), - ), - ); - // Return success to previous screen - Future.delayed(const Duration(milliseconds: 500), () { - if (mounted) { - Navigator.pop(context, true); + // ✅ Call the callback first + onPaymentSuccess?.call(); + // ✅ Then auto-close and return true after 1.5 seconds + Future.delayed(const Duration(milliseconds: 1500), () { + if (context.mounted) { + Navigator.of(context).pop(true); } }); } else if (state is StripePaymentFailure) { - // Show error message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.error), - backgroundColor: Colors.red, - duration: const Duration(seconds: 3), - ), - ); - // Go back to checkout on error - Future.delayed(const Duration(seconds: 1), () { - if (mounted) { - Navigator.pop(context, false); + onPaymentFailure?.call(state.error); + // Auto-close after 2 seconds on failure + Future.delayed(const Duration(seconds: 2), () { + if (context.mounted) { + Navigator.of(context).pop(false); } }); } else if (state is StripePaymentCancelled) { - // Show cancellation message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.orange, - duration: const Duration(seconds: 2), - ), - ); - // Go back to checkout on cancellation - Future.delayed(const Duration(milliseconds: 500), () { - if (mounted) { - Navigator.pop(context, false); - } - }); + onPaymentCancelled?.call(); + Navigator.of(context).pop(false); } }, - child: Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - title: const Text("Processing Payment"), - backgroundColor: Colors.white, - elevation: 0, - automaticallyImplyLeading: false, // Remove back button during processing - centerTitle: true, - ), - body: BlocBuilder( - builder: (context, state) { - return Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Loading Indicator - if (state is StripePaymentLoading) ...[ - const CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - Color(0xFFF95F62), - ), - ), - const SizedBox(height: 24), - const Text( - "Preparing secure payment...", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 12), - Text( - "Please wait", - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), - ), - ], - - // Amount Display - const SizedBox(height: 32), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 16, - ), - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFE0E0E0), - ), - ), - child: Column( - children: [ - Text( - "Payment Amount", - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), - ), - const SizedBox(height: 8), - Text( - "\$${widget.amount.toStringAsFixed(2)}", - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 4), - Text( - widget.currency.toUpperCase(), - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), - ), - ], - ), - ), - - const SizedBox(height: 32), - - // Security Badge - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.lock_outline, - size: 16, - color: Colors.grey[600], - ), - const SizedBox(width: 6), - Text( - "Secured by Stripe", - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ), + builder: (context, state) { + return Container( + height: heightRatio == 1.0 + ? MediaQuery.of(context).size.height + : MediaQuery.of(context).size.height * heightRatio, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: heightRatio == 1.0 + ? null + : const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Stack( + children: [ + // Main content + Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Custom header widget + if (headerWidget != null) ...[ + headerWidget!, + const SizedBox(height: 16), ], - ), - ], + + // Title + if (title != null) ...[ + Text( + title!, + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xFF333333), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + ], + + // Amount display + if (amount != null) ...[ + Text( + '$currencySymbol${amount!.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 32.sp, + fontWeight: FontWeight.w700, + color: primaryColor, + ), + ), + const SizedBox(height: 24), + ], + + // Payment status + _buildPaymentStatus(context, state), + + // Custom footer widget + if (footerWidget != null) ...[ + const SizedBox(height: 16), + footerWidget!, + ], + ], + ), ), ), - ); - }, - ), - ), + + // Close button (only show when allowed) + if (_shouldShowCloseButton(state)) + Positioned( + top: 16, + right: 16, + child: IconButton( + onPressed: () { + if (state is StripePaymentLoading) { + // Cancel payment if loading + context + .read() + .add(CancelPaymentEvent()); + } else { + Navigator.of(context).pop(false); + } + }, + icon: Icon( + Icons.close, + color: Colors.grey[600], + size: 24, + ), + ), + ), + ], + ), + ); + }, ); } + + /// Build payment status widget based on state + Widget _buildPaymentStatus(BuildContext context, StripePaymentState state) { + if (state is StripePaymentLoading) { + return Column( + children: [ + CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation(primaryColor), + ), + const SizedBox(height: 24), + Text( + loadingMessage, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF333333), + ), + textAlign: TextAlign.center, + ), + ], + ); + } else if (state is StripePaymentSuccess) { + return Column( + children: [ + Icon( + Icons.check_circle, + color: successColor, + size: 64, + ), + const SizedBox(height: 16), + Text( + successMessage, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + textAlign: TextAlign.center, + ), + ], + ); + } else if (state is StripePaymentFailure) { + return Column( + children: [ + Icon( + Icons.error, + color: errorColor, + size: 64, + ), + const SizedBox(height: 16), + Text( + failureMessage, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + state.error, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + // Retry payment + context.read().add( + InitiatePaymentWithClientSecret( + clientSecret: clientSecret, + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Retry Payment', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ); + } else if (state is StripePaymentCancelled) { + return Column( + children: [ + Icon( + Icons.cancel, + color: Colors.orange, + size: 64, + ), + const SizedBox(height: 16), + const Text( + 'Payment Cancelled', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + textAlign: TextAlign.center, + ), + ], + ); + } + + return const SizedBox.shrink(); + } + + /// Determine if close button should be shown + bool _shouldShowCloseButton(StripePaymentState state) { + if (state is StripePaymentLoading) { + return showCloseButtonDuringLoading; + } + // Show for failure and cancelled states + return state is StripePaymentFailure || state is StripePaymentCancelled; + } } \ No newline at end of file diff --git a/lib/checkout/bloc/checkOut/checkout_bloc.dart b/lib/checkout/bloc/checkOut/checkout_bloc.dart index 15abcd6..0763aa0 100644 --- a/lib/checkout/bloc/checkOut/checkout_bloc.dart +++ b/lib/checkout/bloc/checkOut/checkout_bloc.dart @@ -15,6 +15,7 @@ class CheckoutBloc extends Bloc { on(_onFetchCheckoutCoupons); on(_onApplyCoupon); on(_onRemoveCoupon); + on(_onApplyCouponToBackend); // 🆕 NEW on(_onInitiatePayment); // 🆕 NEW on(_onConfirmPayment); // 🆕 NEW } @@ -42,13 +43,77 @@ class CheckoutBloc extends Bloc { } } - void _onRemoveCoupon( + Future _onRemoveCoupon( RemoveCouponEvent event, Emitter emit, - ) { + ) async { if (state is CheckoutCouponsLoadedState) { final currentState = state as CheckoutCouponsLoadedState; - emit(currentState.copyWith(clearAppliedCoupon: true)); + + // Show loading + emit(currentState.copyWith(isApplyingCoupon: true, couponError: null)); + + try { + // Call API with empty coupon code + await checkoutRepository.applyCoupon( + bookingId: event.bookingId, + couponCode: '', // Empty string to remove coupon + ); + + // Clear applied coupon from state + emit(currentState.copyWith( + clearAppliedCoupon: true, + isApplyingCoupon: false, + couponError: null, + )); + } catch (e) { + emit(currentState.copyWith( + isApplyingCoupon: false, + couponError: e.toString(), + )); + } + } + } + + /// 🆕 Apply Coupon to Backend + /// Calls the PUT /apply-coupon API + Future _onApplyCouponToBackend( + ApplyCouponToBackendEvent event, + Emitter emit, + ) async { + if (state is CheckoutCouponsLoadedState) { + final currentState = state as CheckoutCouponsLoadedState; + + // Show loading + emit(currentState.copyWith(isApplyingCoupon: true, couponError: null)); + + try { + // Call API + final response = await checkoutRepository.applyCoupon( + bookingId: event.bookingId, + couponCode: event.couponCode, + ); + + // Find the coupon from the list + final appliedCoupon = currentState.coupons.firstWhere( + (c) => c.couponCode == event.couponCode, + orElse: () => currentState.coupons.first, + ); + + // Update state with applied coupon + emit(currentState.copyWith( + appliedCoupon: appliedCoupon, + isApplyingCoupon: false, + couponError: null, + )); + + // Success message will be handled in view + } catch (e) { + emit(currentState.copyWith( + isApplyingCoupon: false, + couponError: e.toString(), + )); + } } } diff --git a/lib/checkout/bloc/checkOut/checkout_event.dart b/lib/checkout/bloc/checkOut/checkout_event.dart index 4e0677f..631d8ba 100644 --- a/lib/checkout/bloc/checkOut/checkout_event.dart +++ b/lib/checkout/bloc/checkOut/checkout_event.dart @@ -8,8 +8,22 @@ class ApplyCouponEvent extends CheckoutEvent { final AllCouponsModel coupon; ApplyCouponEvent({required this.coupon}); } +/// 🆕 Apply Coupon to Backend Event +class ApplyCouponToBackendEvent extends CheckoutEvent { + final int bookingId; + final String couponCode; -class RemoveCouponEvent extends CheckoutEvent {} + ApplyCouponToBackendEvent({ + required this.bookingId, + required this.couponCode, + }); +} + +class RemoveCouponEvent extends CheckoutEvent { + final int bookingId; + + RemoveCouponEvent({required this.bookingId}); +} /// 🆕 Initiate Payment Event /// Triggered when user clicks "Pay" button diff --git a/lib/checkout/bloc/checkOut/checkout_state.dart b/lib/checkout/bloc/checkOut/checkout_state.dart index b5e5a9e..f77bc04 100644 --- a/lib/checkout/bloc/checkOut/checkout_state.dart +++ b/lib/checkout/bloc/checkOut/checkout_state.dart @@ -10,6 +10,10 @@ class CheckoutCouponsLoadedState extends CheckoutState { final List coupons; final AllCouponsModel? appliedCoupon; + // 🆕 Coupon application tracking + final bool isApplyingCoupon; + final String? couponError; + // 🆕 Payment-related fields final bool isInitiatingPayment; final String? clientSecret; // Stripe client secret @@ -25,6 +29,8 @@ class CheckoutCouponsLoadedState extends CheckoutState { CheckoutCouponsLoadedState({ required this.coupons, this.appliedCoupon, + this.isApplyingCoupon = false, + this.couponError, this.isInitiatingPayment = false, this.clientSecret, this.bookingId, @@ -39,6 +45,8 @@ class CheckoutCouponsLoadedState extends CheckoutState { List? coupons, AllCouponsModel? appliedCoupon, bool clearAppliedCoupon = false, + bool? isApplyingCoupon, + String? couponError, bool? isInitiatingPayment, String? clientSecret, int? bookingId, @@ -52,6 +60,8 @@ class CheckoutCouponsLoadedState extends CheckoutState { return CheckoutCouponsLoadedState( coupons: coupons ?? this.coupons, appliedCoupon: clearAppliedCoupon ? null : (appliedCoupon ?? this.appliedCoupon), + isApplyingCoupon: isApplyingCoupon ?? this.isApplyingCoupon, + couponError: couponError, isInitiatingPayment: isInitiatingPayment ?? this.isInitiatingPayment, bookingId: bookingId ?? this.bookingId, paymentError: paymentError, diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index 6cb3e39..2359911 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -10,13 +10,9 @@ import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../StripePayment/bloc/stripe_payment_bloc.dart'; -import '../../StripePayment/bloc/stripe_payment_event.dart'; -import '../../StripePayment/bloc/stripe_payment_state.dart'; -import '../../StripePayment/repository/stripe_service.dart'; +import '../../StripePayment/view/stripe_payment.dart'; import '../../add_details/add_details_view.dart'; import '../../buy_a_pass/models/checkout_model.dart'; -import '../../common_bloc/bottom_navigation_bloc.dart'; import '../../localPreference/local_preference.dart'; import '../widget/pass_purchase_details_bottomsheet.dart'; import '../repository/all_coupons_repository.dart'; @@ -119,231 +115,62 @@ class _CheckoutContent extends StatelessWidget { }); /// 🆕 Handle payment flow with client secret + /// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION Future _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId) async { - // Show payment bottom sheet with BLoC - final paymentResult = await showModalBottomSheet>( + final paymentSuccess = await StripePaymentScreen.showAsBottomSheet( context: context, + clientSecret: clientSecret, + amount: checkoutData.totalPrice.toDouble(), + currencySymbol: '\$', + title: 'Complete Payment', + loadingMessage: 'Processing your pass payment...', + successMessage: 'Payment Successful!\nYour pass is ready.', + failureMessage: 'Payment Failed', + primaryColor: const Color(0xFFF95F62), + heightRatio: 0.5, isDismissible: false, enableDrag: false, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (bottomSheetContext) { - return BlocProvider( - create: (_) => StripePaymentBloc(stripeService: StripeService()) - ..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)), - child: BlocConsumer( - listener: (context, state) { - if (state is StripePaymentSuccess) { - // Return success with stripe status - Navigator.of(bottomSheetContext).pop({ - 'success': true, - 'stripeStatus': 'succeeded', - 'paymentStatus': 'success', - }); - } else if (state is StripePaymentFailure) { - // Return failure with stripe status - Navigator.of(bottomSheetContext).pop({ - 'success': false, - 'stripeStatus': 'requires_payment_method', - 'paymentStatus': 'failed', - 'error': state.error, - }); - } else if (state is StripePaymentCancelled) { - // Return cancelled status - Navigator.of(bottomSheetContext).pop({ - 'success': false, - 'stripeStatus': 'cancelled', - 'paymentStatus': 'cancelled', - }); - } - }, - builder: (context, state) { - return Container( - height: MediaQuery.of(context).size.height * 0.5, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (state is StripePaymentLoading) ...[ - const CircularProgressIndicator( - strokeWidth: 3, - valueColor: AlwaysStoppedAnimation( - Color(0xFFF95F62), - ), - ), - const SizedBox(height: 24), - const Text( - "Processing payment...", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Color(0xFF333333), - ), - ), - ] else if (state is StripePaymentSuccess) ...[ - const Icon( - Icons.check_circle, - color: Colors.green, - size: 64, - ), - const SizedBox(height: 16), - const Text( - "Payment Successful!", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF333333), - ), - ), - ] else if (state is StripePaymentFailure) ...[ - const Icon( - Icons.error, - color: Colors.red, - size: 64, - ), - const SizedBox(height: 16), - const Text( - "Payment Failed", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 8), - Text( - state.error, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () { - Navigator.of(bottomSheetContext).pop({ - 'success': false, - 'stripeStatus': 'requires_payment_method', - 'paymentStatus': 'failed', - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF95F62), - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 12, - ), - ), - child: const Text( - "Close", - style: TextStyle(color: Colors.white), - ), - ), - ] else if (state is StripePaymentCancelled) ...[ - const Icon( - Icons.cancel, - color: Colors.orange, - size: 64, - ), - const SizedBox(height: 16), - const Text( - "Payment Cancelled", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () { - Navigator.of(bottomSheetContext).pop({ - 'success': false, - 'stripeStatus': 'cancelled', - 'paymentStatus': 'cancelled', - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF95F62), - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 12, - ), - ), - child: const Text( - "Close", - style: TextStyle(color: Colors.white), - ), - ), - ], - ], - ), - ), - ), - ); - }, + onPaymentSuccess: () { + context.read().add( + ConfirmPaymentEvent( + bookingId: bookingId, + stripeStatus: 'succeeded', + paymentStatus: 'success', + ), + ); + }, + onPaymentFailure: (error) { + context.read().add( + ConfirmPaymentEvent( + bookingId: bookingId, + stripeStatus: 'failed', + paymentStatus: 'failed', + ), + ); + }, + onPaymentCancelled: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Payment cancelled'), + backgroundColor: Colors.orange, ), ); }, ); - // Handle payment result - if (paymentResult != null) { - final success = paymentResult['success'] as bool? ?? false; - final stripeStatus = paymentResult['stripeStatus'] as String? ?? 'unknown'; - final paymentStatus = paymentResult['paymentStatus'] as String? ?? 'unknown'; + // ✅ USE paymentSuccess HERE + if (paymentSuccess == true && context.mounted) { + // Wait a moment for backend confirmation + await Future.delayed(const Duration(milliseconds: 500)); - if (success) { - // Payment successful - confirm with backend - if (context.mounted) { - // context.read().add( - // ConfirmPaymentEvent( - // bookingId: bookingId, - // stripeStatus: stripeStatus, - // paymentStatus: paymentStatus, - // ), - // ); - ScaffoldMessenger.of(context).showSnackBar( + // Navigate to home after successful payment + Navigator.of(context).popUntil((route) => route.isFirst); + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Payment confirmed successfully!'), backgroundColor: Colors.green, ), ); - context.read().add(NavigationTabChanged(2)); - } - - } else { - // Payment failed or cancelled - still confirm with backend - if (context.mounted) { - // context.read().add( - // ConfirmPaymentEvent( - // bookingId: bookingId, - // stripeStatus: stripeStatus, - // paymentStatus: paymentStatus, - // ), - // ); - - // Show error message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - paymentResult['error'] as String? ?? 'Payment failed. Please try again.', - ), - backgroundColor: Colors.red, - ), - ); - } - } } } @@ -740,13 +567,16 @@ class _CheckoutContent extends StatelessWidget { GestureDetector( onTap: () { if (appliedCoupon != null) { - context - .read() - .add(RemoveCouponEvent()); - } else if (state.coupons.isNotEmpty) { context.read().add( - ApplyCouponEvent( - coupon: state.coupons[0]), + RemoveCouponEvent(bookingId: bookingId), + ); + } else if (state.coupons.isNotEmpty) { + // Apply coupon via backend API + context.read().add( + ApplyCouponToBackendEvent( + bookingId: bookingId, + couponCode: state.coupons[0].couponCode, + ), ); } }, @@ -762,8 +592,9 @@ class _CheckoutContent extends StatelessWidget { borderRadius: BorderRadius.circular(8.r), ), child: CustomText( - text: - appliedCoupon != null ? "Remove" : "Apply", + text: state.isApplyingCoupon + ? "Applying..." + : (appliedCoupon != null ? "Remove" : "Apply"), color: const Color(0xFFF95F62), size: 14.sp, ), diff --git a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart index ffbca46..23e850b 100644 --- a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart +++ b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart @@ -1,20 +1,64 @@ import 'package:bloc/bloc.dart'; import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart'; +import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart'; +import 'package:citycards_customer/localPreference/local_preference.dart'; import 'package:equatable/equatable.dart'; - part 'get_itinerary_event.dart'; part 'get_itinerary_state.dart'; class GetItineraryBloc extends Bloc { - GetItineraryBloc() : super(GetItineraryInitial()) { - on((event, emit) { - try { - emit(GetItineraryLoading()); - final data = ItineraryRepository().fetchItinerary(); - emit(GetItinerarySuccessfully()); - } catch (e) { - emit(GetItineraryFailed(error: "Something went wrong")); - } - }); + final ItineraryRepository _repository; + + GetItineraryBloc({ItineraryRepository? repository}) + : _repository = repository ?? ItineraryRepository(), + super(GetItineraryInitial()) { + on(_onCheckLoginAndFetch); + on(_onGetItinerary); } -} + + Future _onCheckLoginAndFetch( + CheckLoginAndFetchItinerary event, + Emitter emit, + ) async { + 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(); + emit(GetItinerarySuccessfully(itineraries: response.itineraries)); + } catch (e) { + emit(GetItineraryFailed( + error: e.toString().contains('Exception') + ? e.toString().replaceAll('Exception: ', '') + : "Failed to load itineraries. Please try again.")); + } + } + + Future _onGetItinerary( + GetIiterary event, + Emitter emit, + ) async { + try { + emit(GetItineraryLoading()); + + final response = await _repository.fetchMyItineraries(); + + emit(GetItinerarySuccessfully(itineraries: response.itineraries)); + } catch (e) { + emit(GetItineraryFailed( + error: e.toString().contains('Exception') + ? e.toString().replaceAll('Exception: ', '') + : "Failed to load itineraries. Please try again.")); + } + } +} \ No newline at end of file diff --git a/lib/itinerary_creation/bloc/get_itinerary_event.dart b/lib/itinerary_creation/bloc/get_itinerary_event.dart index a774fef..33819e2 100644 --- a/lib/itinerary_creation/bloc/get_itinerary_event.dart +++ b/lib/itinerary_creation/bloc/get_itinerary_event.dart @@ -8,3 +8,5 @@ abstract class GetItineraryEvent extends Equatable { } class GetIiterary extends GetItineraryEvent {} + +class CheckLoginAndFetchItinerary extends GetItineraryEvent {} \ No newline at end of file diff --git a/lib/itinerary_creation/bloc/get_itinerary_state.dart b/lib/itinerary_creation/bloc/get_itinerary_state.dart index 035989d..616e7a9 100644 --- a/lib/itinerary_creation/bloc/get_itinerary_state.dart +++ b/lib/itinerary_creation/bloc/get_itinerary_state.dart @@ -11,9 +11,22 @@ final class GetItineraryInitial extends GetItineraryState {} class GetItineraryLoading extends GetItineraryState {} -class GetItinerarySuccessfully extends GetItineraryState {} +class GetItineraryNotLoggedIn extends GetItineraryState {} + +class GetItinerarySuccessfully extends GetItineraryState { + final List itineraries; + + const GetItinerarySuccessfully({required this.itineraries}); + + @override + List get props => [itineraries]; +} class GetItineraryFailed extends GetItineraryState { final String error; + const GetItineraryFailed({required this.error}); -} + + @override + List get props => [error]; +} \ No newline at end of file diff --git a/lib/itinerary_creation/models/my_itinerary_model.dart b/lib/itinerary_creation/models/my_itinerary_model.dart new file mode 100644 index 0000000..3b202ea --- /dev/null +++ b/lib/itinerary_creation/models/my_itinerary_model.dart @@ -0,0 +1,240 @@ +class MyItineraryResponse { + List itineraries; + + MyItineraryResponse({required this.itineraries}); + + factory MyItineraryResponse.fromJson(List? json) { + return MyItineraryResponse( + itineraries: json == null + ? [] + : json.map((e) => MyItinerary.fromJson(e)).toList(), + ); + } + + List> toJson() => + itineraries.map((e) => e.toJson()).toList(); +} + +class MyItinerary { + int id; + int userXid; + int cityXid; + String address; + double latitude; + double longitude; + String tripEnergy; + bool travelingWithKids; + List dietaryPreferences; + Preferences preferences; + int totalDays; + String aiModel; + String promptVersion; + bool isActive; + String createdAt; + String updatedAt; + List days; + + MyItinerary({ + required this.id, + required this.userXid, + required this.cityXid, + required this.address, + required this.latitude, + required this.longitude, + required this.tripEnergy, + required this.travelingWithKids, + required this.dietaryPreferences, + required this.preferences, + required this.totalDays, + required this.aiModel, + required this.promptVersion, + required this.isActive, + required this.createdAt, + required this.updatedAt, + required this.days, + }); + + factory MyItinerary.fromJson(Map? json) { + json ??= {}; + + return MyItinerary( + id: (json['id'] as num?)?.toInt() ?? 0, + userXid: (json['userXid'] as num?)?.toInt() ?? 0, + cityXid: (json['cityXid'] as num?)?.toInt() ?? 0, + address: json['Address'] ?? "", + latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0, + longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0, + tripEnergy: json['tripEnergy'] ?? "", + travelingWithKids: json['travelingWithKids'] ?? false, + dietaryPreferences: json['dietaryPreferences'] == null + ? [] + : List.from(json['dietaryPreferences']), + preferences: + Preferences.fromJson(json['preferences'] ?? {}), + totalDays: (json['totalDays'] as num?)?.toInt() ?? 0, + aiModel: json['aiModel'] ?? "", + promptVersion: json['promptVersion'] ?? "", + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? "", + updatedAt: json['updatedAt'] ?? "", + days: json['days'] == null + ? [] + : List>.from(json['days']) + .map((e) => ItineraryDay.fromJson(e)) + .toList(), + ); + } + + Map toJson() => { + "id": id, + "userXid": userXid, + "cityXid": cityXid, + "Address": address, + "latitude": latitude, + "longitude": longitude, + "tripEnergy": tripEnergy, + "travelingWithKids": travelingWithKids, + "dietaryPreferences": dietaryPreferences, + "preferences": preferences.toJson(), + "totalDays": totalDays, + "aiModel": aiModel, + "promptVersion": promptVersion, + "isActive": isActive, + "createdAt": createdAt, + "updatedAt": updatedAt, + "days": days.map((e) => e.toJson()).toList(), + }; +} + +class Preferences { + int shopping; + int wildlife; + int landmarks; + int scenicViews; + int artAndMuseums; + + Preferences({ + required this.shopping, + required this.wildlife, + required this.landmarks, + required this.scenicViews, + required this.artAndMuseums, + }); + + factory Preferences.fromJson(Map? json) { + json ??= {}; + + return Preferences( + shopping: (json['shopping'] as num?)?.toInt() ?? 0, + wildlife: (json['wildlife'] as num?)?.toInt() ?? 0, + landmarks: (json['landmarks'] as num?)?.toInt() ?? 0, + scenicViews: (json['scenicViews'] as num?)?.toInt() ?? 0, + artAndMuseums: (json['artAndMuseums'] as num?)?.toInt() ?? 0, + ); + } + + Map toJson() => { + "shopping": shopping, + "wildlife": wildlife, + "landmarks": landmarks, + "scenicViews": scenicViews, + "artAndMuseums": artAndMuseums, + }; +} + +class ItineraryDay { + int id; + int itineraryXid; + int dayNumber; + String title; + String summary; + List items; + + ItineraryDay({ + required this.id, + required this.itineraryXid, + required this.dayNumber, + required this.title, + required this.summary, + required this.items, + }); + + factory ItineraryDay.fromJson(Map? json) { + json ??= {}; + + return ItineraryDay( + id: (json['id'] as num?)?.toInt() ?? 0, + itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0, + dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0, + title: json['title'] ?? "", + summary: json['summary'] ?? "", + items: json['items'] == null + ? [] + : List>.from(json['items']) + .map((e) => DayItem.fromJson(e)) + .toList(), + ); + } + + Map toJson() => { + "id": id, + "itineraryXid": itineraryXid, + "dayNumber": dayNumber, + "title": title, + "summary": summary, + "items": items.map((e) => e.toJson()).toList(), + }; +} + +class DayItem { + int id; + int itineraryDayXid; + String timeSlot; + String title; + String description; + String locationName; + String imageUrl; + double latitude; + double longitude; + + DayItem({ + required this.id, + required this.itineraryDayXid, + required this.timeSlot, + required this.title, + required this.description, + required this.locationName, + required this.imageUrl, + required this.latitude, + required this.longitude, + }); + + factory DayItem.fromJson(Map? json) { + json ??= {}; + + return DayItem( + id: (json['id'] as num?)?.toInt() ?? 0, + itineraryDayXid: + (json['itineraryDayXid'] as num?)?.toInt() ?? 0, + timeSlot: json['timeSlot'] ?? "", + title: json['title'] ?? "", + description: json['description'] ?? "", + locationName: json['locationName'] ?? "", + imageUrl: json['imageUrl'] ?? "", + latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0, + longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0, + ); + } + + Map toJson() => { + "id": id, + "itineraryDayXid": itineraryDayXid, + "timeSlot": timeSlot, + "title": title, + "description": description, + "locationName": locationName, + "imageUrl": imageUrl, + "latitude": latitude, + "longitude": longitude, + }; +} diff --git a/lib/itinerary_creation/repository/itinerary_repository.dart b/lib/itinerary_creation/repository/itinerary_repository.dart index ddff6a1..4993c7e 100644 --- a/lib/itinerary_creation/repository/itinerary_repository.dart +++ b/lib/itinerary_creation/repository/itinerary_repository.dart @@ -5,14 +5,18 @@ import 'package:dio/dio.dart'; import '../../networkApiServices/api_urls.dart'; import '../../networkApiServices/network_api_services.dart'; +import '../models/my_itinerary_model.dart'; class ItineraryRepository { final NetworkApiService _apiService = NetworkApiService(); - Future fetchItinerary() async { - final response = await _apiService.getApi(url: ApiUrls.getItinerary); + Future fetchMyItineraries() async { + final response = await _apiService.getApi( + url: ApiUrls.myItineraries, // 👈 Make sure this endpoint exists + ); - return response.data; + /// Because API returns LIST + return MyItineraryResponse.fromJson(response.data); } Future> fetchItineraryCities() async { diff --git a/lib/itinerary_creation/views/magic_itinerary_view.dart b/lib/itinerary_creation/views/magic_itinerary_view.dart index fba8496..68ea535 100644 --- a/lib/itinerary_creation/views/magic_itinerary_view.dart +++ b/lib/itinerary_creation/views/magic_itinerary_view.dart @@ -3,11 +3,14 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/core/route_constants.dart'; import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart'; +import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_bloc.dart'; +import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart'; 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 '../../localPreference/local_preference.dart'; import '../../login/view/login_email_bottomsheet.dart'; +import 'package:intl/intl.dart'; class MagicItineraryView extends StatefulWidget { const MagicItineraryView({super.key}); @@ -17,22 +20,11 @@ class MagicItineraryView extends StatefulWidget { } class _MagicItineraryViewState extends State { - bool isLoggedIn = false; - bool isLoading = true; - @override void initState() { super.initState(); - _checkLoginStatus(); - } - - Future _checkLoginStatus() async { - // final loginStatus = await LocalPreference.getLogin(); - final loginStatus = true; - setState(() { - isLoggedIn = loginStatus; - isLoading = false; - }); + // Trigger login check and fetch on init + context.read().add(CheckLoginAndFetchItinerary()); } @override @@ -40,73 +32,109 @@ class _MagicItineraryViewState extends State { return Scaffold( backgroundColor: Color(0xFFFFF5F5), body: SafeArea( - child: isLoading - ? Center(child: CircularProgressIndicator()) - : Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: SingleChildScrollView( - child: Column( - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showDivider: false, - ), - SizedBox(height: 24.h), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: SingleChildScrollView( + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: false, + ), + SizedBox(height: 24.h), - // Show different UI based on login status - if (isLoggedIn) ...[ - ItineraryFilledCard(), - SizedBox(height: 32.h), - CustomPaint( - painter: DottedBorderPainter(), - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 24.h), - decoration: BoxDecoration( - color: Color(0xFFF95F62).withOpacity(0.25), - borderRadius: BorderRadius.circular(12.sp), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + // BLoC Builder for all states + BlocBuilder( + builder: (context, state) { + if (state is GetItineraryLoading) { + return Center( + child: Padding( + padding: EdgeInsets.only(top: 100.h), + child: CircularProgressIndicator(), + ), + ); + } else if (state is GetItineraryNotLoggedIn) { + return NotLoggedInItineraryView(); + } else if (state is GetItinerarySuccessfully) { + if (state.itineraries.isEmpty) { + return NoItineraryView(); + } + return Column( + children: [ + ...state.itineraries.map((itinerary) { + return Column( children: [ - CustomText( - text: "Plan your next adventure", - color: Color(0xFF656565), - size: 14.sp, + ItineraryFilledCard( + itinerary: itinerary, ), SizedBox(height: 16.h), - CustomFilledButton( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - ItineraryCreationStartPage(), - ), - ); - }, - label: "Create My Itinerary", - showArrow: true, - ), ], + ); + }).toList(), + SizedBox(height: 16.h), + CustomPaint( + painter: DottedBorderPainter(), + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: Color(0xFFF95F62).withOpacity(0.1), + borderRadius: BorderRadius.circular(12.sp), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomText( + text: "Plan your next adventure", + color: Color(0xFF656565), + size: 14.sp, + ), + SizedBox(height: 16.h), + CustomFilledButton( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ItineraryCreationStartPage(), + ), + ); + }, + label: "Create My Itinerary", + showArrow: true, + ), + ], + ), ), ), - ), - ] else ...[ - EmptyItineraryView(), - ], - ], - ), + ], + ); + } else if (state is GetItineraryFailed) { + return ErrorItineraryView( + error: state.error, + onRetry: () { + context + .read() + .add(CheckLoginAndFetchItinerary()); + }, + ); + } + // Initial state + return SizedBox.shrink(); + }, ), - ), + ], + ), + ), + ), ), ); } } -class EmptyItineraryView extends StatelessWidget { - const EmptyItineraryView({super.key}); +class NotLoggedInItineraryView extends StatelessWidget { + const NotLoggedInItineraryView({super.key}); @override Widget build(BuildContext context) { @@ -116,7 +144,7 @@ class EmptyItineraryView extends StatelessWidget { // Illustration image - replace with your asset path Image.asset( - "assets/images/not_login.png", // Replace with your actual asset path + "assets/images/not_login.png", height: 300.h, fit: BoxFit.contain, ), @@ -164,11 +192,157 @@ class EmptyItineraryView extends StatelessWidget { } } -class ItineraryFilledCard extends StatelessWidget { - const ItineraryFilledCard({super.key}); +class NoItineraryView extends StatelessWidget { + const NoItineraryView({super.key}); @override Widget build(BuildContext context) { + return Column( + children: [ + SizedBox(height: 40.h), + + // Illustration for no itineraries + Icon( + Icons.travel_explore, + size: 120.sp, + color: Colors.grey.withOpacity(0.3), + ), + + SizedBox(height: 32.h), + + CustomText( + text: "No Itineraries Yet", + size: 18.sp, + weight: FontWeight.w600, + textAlign: TextAlign.center, + ), + + SizedBox(height: 12.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: CustomText( + text: + "You haven't created any itineraries yet. Start planning your next adventure!", + size: 14.sp, + color: Color(0xFF656565), + textAlign: TextAlign.center, + ), + ), + + SizedBox(height: 32.h), + + CustomFilledButton( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ItineraryCreationStartPage(), + ), + ); + }, + label: "Create My Itinerary", + showArrow: true, + ), + ], + ); + } +} + +class ErrorItineraryView extends StatelessWidget { + final String error; + final VoidCallback onRetry; + + const ErrorItineraryView({ + super.key, + required this.error, + required this.onRetry, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox(height: 40.h), + + Icon( + Icons.error_outline, + size: 120.sp, + color: Colors.red.withOpacity(0.3), + ), + + SizedBox(height: 32.h), + + CustomText( + text: "Oops! Something went wrong", + size: 18.sp, + weight: FontWeight.w600, + textAlign: TextAlign.center, + ), + + SizedBox(height: 12.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: CustomText( + text: error, + size: 14.sp, + color: Color(0xFF656565), + textAlign: TextAlign.center, + ), + ), + + SizedBox(height: 32.h), + + CustomFilledButton( + onTap: onRetry, + label: "Try Again", + showArrow: false, + ), + ], + ); + } +} + +class ItineraryFilledCard extends StatelessWidget { + final MyItinerary itinerary; + + const ItineraryFilledCard({ + super.key, + required this.itinerary, + }); + + String _formatDate(String dateString) { + try { + final date = DateTime.parse(dateString); + return DateFormat('M/d/yyyy').format(date); + } catch (e) { + return dateString; + } + } + + int _getTotalAttractions() { + int total = 0; + for (var day in itinerary.days) { + total += day.items.length; + } + return total; + } + + String _getCityName() { + // You might want to fetch city name from cityXid or use address + // For now, extracting from address + if (itinerary.address.isNotEmpty) { + return itinerary.address.split(',').last.trim(); + } + return "Unknown City"; + } + + @override + Widget build(BuildContext context) { + final totalAttractions = _getTotalAttractions(); + final cityName = _getCityName(); + return Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), decoration: BoxDecoration( @@ -181,19 +355,23 @@ class ItineraryFilledCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - CustomText( - text: "Melbourne Unlimited Card", - size: 16.sp, - weight: FontWeight.w500, + Expanded( + child: CustomText( + text: "$cityName Travel Plan", + size: 16.sp, + weight: FontWeight.w500, + ), ), Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 2.h), decoration: BoxDecoration( - color: Color(0xFF439F6E), + color: itinerary.isActive + ? Color(0xFF439F6E) + : Colors.grey.shade400, borderRadius: BorderRadius.circular(100.r), ), child: CustomText( - text: "Active", + text: itinerary.isActive ? "Active" : "Inactive", size: 11.sp, color: Colors.white, ), @@ -202,7 +380,7 @@ class ItineraryFilledCard extends StatelessWidget { ), SizedBox(height: 4.h), CustomText( - text: "Melbourne", + text: cityName, size: 12.sp, color: Colors.black.withOpacity(0.4), ), @@ -211,7 +389,11 @@ class ItineraryFilledCard extends StatelessWidget { children: [ Image.asset("assets/icons/calender_filled.png", width: 16.sp), SizedBox(width: 4.w), - CustomText(text: "7 days", color: Color(0xFF8E8E8E), size: 12.sp), + CustomText( + text: "${itinerary.totalDays} days", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), ], ), SizedBox(height: 8.h), @@ -224,7 +406,7 @@ class ItineraryFilledCard extends StatelessWidget { ), SizedBox(width: 4.w), CustomText( - text: "6 attractions", + text: "$totalAttractions attractions", color: Color(0xFF8E8E8E), size: 12.sp, ), @@ -236,18 +418,34 @@ class ItineraryFilledCard extends StatelessWidget { Icon(Icons.watch_later, color: Color(0xFF8E8E8E), size: 16.sp), SizedBox(width: 4.w), CustomText( - text: "Created 1/15/2024", + text: "Created ${_formatDate(itinerary.createdAt)}", color: Color(0xFF8E8E8E), size: 12.sp, ), ], ), + // if (itinerary.travelingWithKids) ...[ + // SizedBox(height: 8.h), + // Row( + // children: [ + // Icon(Icons.family_restroom, + // color: Color(0xFF8E8E8E), size: 16.sp), + // SizedBox(width: 4.w), + // CustomText( + // text: "Family Friendly", + // color: Color(0xFF8E8E8E), + // size: 12.sp, + // ), + // ], + // ), + // ], SizedBox(height: 12.h), InkWell( onTap: () { - Navigator.of( - context, - ).pushReplacementNamed(RouteConstants.yourItinerary); + Navigator.of(context).pushReplacementNamed( + RouteConstants.yourItinerary, + arguments: itinerary, + ); }, child: Container( height: 43.h, @@ -269,4 +467,4 @@ class ItineraryFilledCard extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/localPreference/local_database.dart b/lib/localPreference/local_database.dart index 4380d0f..b6e5e2d 100644 --- a/lib/localPreference/local_database.dart +++ b/lib/localPreference/local_database.dart @@ -90,6 +90,14 @@ class LocalDatabase { description TEXT ) '''); + /// CITY TABLE + await db.execute(''' + CREATE TABLE selected_city ( + id INTEGER PRIMARY KEY, + city_id INTEGER, + city_logo TEXT + ) +'''); }, ); diff --git a/lib/localPreference/local_preference.dart b/lib/localPreference/local_preference.dart index 2e20eba..5b40d34 100644 --- a/lib/localPreference/local_preference.dart +++ b/lib/localPreference/local_preference.dart @@ -395,6 +395,32 @@ class LocalPreference { } } + static Future setSelectedCityLogo(String logoUrl) async { + final db = await LocalDatabase().database; + + await db.update( + 'selected_city', + {'city_logo': logoUrl}, + where: 'id = ?', + whereArgs: [1], + ); + } + + static Future getSelectedCityLogo() async { + final db = await LocalDatabase().database; + + final result = await db.query( + 'selected_city', + where: 'id = ?', + whereArgs: [1], + ); + + if (result.isNotEmpty) { + return result.first['city_logo'] as String?; + } + return null; + } + static Future clearUserDetails() async { final db = await LocalDatabase().database; diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart index 3c8146d..262ad3f 100644 --- a/lib/login/view/verify_otp_bottomsheet.dart +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -1,5 +1,6 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_bloc.dart'; import 'package:citycards_customer/postcard/blocs/myPostCards/my_postcard_bloc.dart'; import 'package:citycards_customer/profile/bloc/profile/profile_bloc.dart'; import 'package:citycards_customer/profile/bloc/profile/profile_event.dart'; @@ -42,10 +43,11 @@ class _VerifyOtpBottomsheetState extends State { context.read().add(FetchProfileEvent(userId: userId!)); context.read().add(CheckLoginStatusEvent()); context.read().add(CheckLoginStatus()); - context.read().add(FetchDraftPostCards()); + context.read().add(CheckLoginAndFetchItinerary()); + // context.read().add(FetchDraftPostCards()); context.read().add(RefreshDraftPostCards()); context.read().add(RefreshOrderPostCards()); - context.read().add(FetchOrderPostCards()); + // context.read().add(FetchOrderPostCards()); // User exists - navigate to home/dashboard // Navigator.of(context).pushReplacementNamed(RouteConstants.home); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/main.dart b/lib/main.dart index 0422f2e..c1fc509 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,8 @@ import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart'; import 'home/bloc/registeredHome/home_bloc.dart'; import 'home/repository/first_time_user_home_repository.dart'; import 'home/repository/home_repository.dart'; +import 'itinerary_creation/bloc/get_itinerary_bloc.dart'; +import 'itinerary_creation/views/magic_itinerary_view.dart'; import 'login/bloc/login/login_bloc.dart'; import 'login/repository/login_repository.dart'; import 'my_pass/blocs/my_pass_bloc.dart'; @@ -81,6 +83,10 @@ class MyApp extends StatelessWidget { repository: MyPostCardsRepository(), ), ), + BlocProvider( + create: (context) => GetItineraryBloc(), + child: MagicItineraryView(), + ) ], child: MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 5e49f34..25c2b00 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -20,9 +20,8 @@ class ApiUrls { static const myPostCards = "$baseUrl/mobile/postcards/all"; static const coupons = "$baseUrl/mobile/passes/dropdown/card"; - static const getItinerary = "$baseUrl/mobile/itinerary/all-initineraries"; - static const getItineraryCities = - "$baseUrl/mobile/itinerary/cities-with-icons"; + static const myItineraries = "$baseUrl/mobile/itinerary/all-initineraries"; + static const getItineraryCities = "$baseUrl/mobile/itinerary/cities-with-icons"; //Post Apis static const createAccount = "$baseUrl/mobile/user/register"; diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index 34eb3e6..35c9d41 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -17,7 +17,7 @@ class PostcardCreationBloc PostcardCreationBloc() : super( - const PostcardCreationState(currentStep: PostcardStep.uploadPhoto), + const PostcardCreationState(currentStep: PostcardStep.uploadPhoto, address: ''), ) { /* Navigation steps */ @@ -139,6 +139,7 @@ class PostcardCreationBloc fullName: event.fullName, emailId: event.emailId, phoneNumber: event.phoneNumber, + address: event.address, city: event.city, country: event.country, state: event.state, diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart index 8fd7f20..30fa6d2 100644 --- a/lib/postcard/blocs/postcard_creation_events.dart +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -41,6 +41,7 @@ class UpdatePurchaseFormData extends PostcardCreationEvent { final String? pcTitle; final String? fullName; final String? emailId; + final String address; final String? phoneNumber; final String? city; final String? country; @@ -56,6 +57,7 @@ class UpdatePurchaseFormData extends PostcardCreationEvent { this.country, this.state, this.zipCode, + required this.address, }); } diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart index d9c3064..3024bf7 100644 --- a/lib/postcard/blocs/postcard_creation_state.dart +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -15,6 +15,7 @@ class PostcardCreationState { final String? fullName; final String? emailId; final String? phoneNumber; + final String address; final String? city; final String? country; final String? state; @@ -39,7 +40,8 @@ class PostcardCreationState { this.country, this.state, this.zipCode, - this.pcNumber, // 🆕 ADD THIS + this.pcNumber, + required this.address, // 🆕 ADD THIS }); PostcardCreationState copyWith({ @@ -56,6 +58,7 @@ class PostcardCreationState { String? fullName, String? emailId, String? phoneNumber, + String? address, String? city, String? country, String? state, @@ -76,6 +79,7 @@ class PostcardCreationState { fullName: fullName ?? this.fullName, emailId: emailId ?? this.emailId, phoneNumber: phoneNumber ?? this.phoneNumber, + address: address ?? this.address, city: city ?? this.city, country: country ?? this.country, state: state ?? this.state, diff --git a/lib/postcard/views/my_postcard_preview_view.dart b/lib/postcard/views/my_postcard_preview_view.dart index 8ce1491..0d1e36a 100644 --- a/lib/postcard/views/my_postcard_preview_view.dart +++ b/lib/postcard/views/my_postcard_preview_view.dart @@ -156,51 +156,54 @@ class _MyPostcardPreviewViewState extends State { ), ), - // Expanded( - // child: Padding( - // padding: EdgeInsets.only(top: 40.h), - // child: Align( - // alignment: Alignment.topCenter, - // child: AnimatedSwitcher( - // duration: const Duration(milliseconds: 400), - // transitionBuilder: (child, animation) { - // return FadeTransition( - // opacity: animation, - // child: child, - // ); - // }, - // child: showBack - // ? BackCardWidget( - // key: const ValueKey('back'), - // message: widget.postcard.pcContent, - // city: widget.postcard.cityName, - // state: widget.postcard.stateName, - // country: widget.postcard.countryName, - // ) - // : FrontCardWidget( - // key: const ValueKey('front'), - // imageUrl: - // '${ApiUrls.baseUrl}${widget.postcard.pcImagePath}', - // ), - // ), - // ), - // ), - // ), Expanded( child: Padding( padding: EdgeInsets.only(top: 40.h), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: (Widget child, Animation animation) { - return FadeTransition( - opacity: animation, - child: child, - ); - }, - child: showBack ? _buildBackSide() : _buildFrontSide(), + child: Align( + alignment: Alignment.topCenter, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + child: showBack + ? BackCardWidget( + key: const ValueKey('back'), + message: widget.postcard.pcContent, + city: widget.postcard.cityName, + state: widget.postcard.stateName, + country: widget.postcard.countryName, + address: widget.postcard.address1, + name: widget.postcard.fullname, + pincode: widget.postcard.zipCode, + ) + : FrontCardWidget( + key: const ValueKey('front'), + imageUrl: + '${ApiUrls.baseUrl}${widget.postcard.pcImagePath}', + ), + ), ), ), ), + // Expanded( + // child: Padding( + // padding: EdgeInsets.only(top: 40.h), + // child: AnimatedSwitcher( + // duration: const Duration(milliseconds: 400), + // transitionBuilder: (Widget child, Animation animation) { + // return FadeTransition( + // opacity: animation, + // child: child, + // ); + // }, + // child: showBack ? _buildBackSide() : _buildFrontSide(), + // ), + // ), + // ), SizedBox(height: 40.h), ], ), diff --git a/lib/postcard/views/order_success_page_view.dart b/lib/postcard/views/order_success_page_view.dart index 41874e8..21c5962 100644 --- a/lib/postcard/views/order_success_page_view.dart +++ b/lib/postcard/views/order_success_page_view.dart @@ -1,5 +1,7 @@ import 'dart:io'; import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart'; +import 'package:citycards_customer/postcard/widgets/back_card_widget.dart'; +import 'package:citycards_customer/postcard/widgets/front_card_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -82,9 +84,13 @@ class OrderSuccessPageView extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30), child: Transform.rotate( angle: 0.20, - child: MessageCardWidget( + child: BackCardWidget( message: state.message ?? "", - selectedFont: state.selectedFont, + state: "State", + country: "country", + city: "City", + key: const ValueKey('back'), + // selectedFont: state.selectedFont, ), ), ), @@ -93,10 +99,11 @@ class OrderSuccessPageView extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 30), child: Transform.rotate( angle: -0.15, - child: PostCardPreviewWidget( - imagePath: state.imagePath ?? "", - message: state.message ?? "", - selectedFont: state.selectedFont, + child: FrontCardWidget( + key: const ValueKey('front'), + imageUrl: state.imagePath ?? "", + // message: state.message ?? "", + // selectedFont: state.selectedFont, ), ), ), diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart index 9617aff..8a0aab4 100644 --- a/lib/postcard/views/postcard_checkout_page_view.dart +++ b/lib/postcard/views/postcard_checkout_page_view.dart @@ -1,5 +1,7 @@ import 'dart:io'; import 'package:citycards_customer/postcard/views/my_postcards_view.dart'; +import 'package:citycards_customer/postcard/widgets/back_card_widget.dart'; +import 'package:citycards_customer/postcard/widgets/front_card_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -84,8 +86,8 @@ class _PostcardCheckoutPageViewState extends State { countryName: widget.countryName, cityName: widget.cityName, stateName: widget.stateName, - zipCode: widget.zipCode, - address1: widget.address1, + zipCode: creationState.zipCode, + address1: creationState.address, address2: widget.address2, pcTitle: widget.pcTitle, pcContent: creationState.message ?? '', @@ -416,15 +418,23 @@ class _PostcardCheckoutPageViewState extends State { const SizedBox(height: 16), - MessageCardWidget( + BackCardWidget( message: creationState.message ?? "", - selectedFont: creationState.selectedFont, + state: widget.stateName, + country: widget.countryName, + city: widget.cityName, + address: creationState.address, + name: widget.fullname, + pincode: widget.zipCode, + key: const ValueKey('back'), + // selectedFont: creationState.selectedFont, ), - SizedBox(height: 10.h), - PostCardPreviewWidget( - imagePath: creationState.imagePath ?? "", - message: creationState.message ?? "", - selectedFont: creationState.selectedFont, + SizedBox(height: 20.h), + FrontCardWidget( + imageUrl: creationState.imagePath ?? "", + key: const ValueKey('front'), + // message: creationState.message ?? "", + // selectedFont: creationState.selectedFont, ), SizedBox(height: 60.h), @@ -441,14 +451,14 @@ class _PostcardCheckoutPageViewState extends State { Divider(color: Color(0xffEDEDED)), const SizedBox(height: 5), - _buildPaymentRow( - "Subtotal", "\$ ${widget.baseAmount.toStringAsFixed(2)}"), - const SizedBox(height: 20), - _buildPaymentRow( - "Discount", "\$ ${widget.totalTaxAmount.toStringAsFixed(2)}", - highlight: true), - const SizedBox(height: 8), - Divider(color: Colors.black), + // _buildPaymentRow( + // "Subtotal", "\$ ${widget.baseAmount.toStringAsFixed(2)}"), + // const SizedBox(height: 20), + // _buildPaymentRow( + // "Discount", "\$ ${widget.totalTaxAmount.toStringAsFixed(2)}", + // highlight: true), + // const SizedBox(height: 8), + // Divider(color: Colors.black), _buildPaymentRow( "Grand Total", "\$ ${widget.totalAmount.toStringAsFixed(2)}", size: 20.sp), @@ -465,7 +475,7 @@ class _PostcardCheckoutPageViewState extends State { const SizedBox(width: 10), Expanded( child: Text( - "${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}", + "${creationState.address},${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}", style: GoogleFonts.poppins( fontSize: 13.sp, color: const Color(0xff2D3134), diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart index 461c79d..e41f101 100644 --- a/lib/postcard/views/postcard_purchase_form_page_view.dart +++ b/lib/postcard/views/postcard_purchase_form_page_view.dart @@ -23,6 +23,7 @@ class _PostcardPurchaseFormPageViewState extends State Date: Tue, 10 Feb 2026 13:58:58 +0530 Subject: [PATCH 3/8] updated iternary api and updated buy pass flow --- .../repository/buy_pass_repository.dart | 7 +- lib/buy_a_pass/widget/payment_card_view.dart | 3 +- lib/checkout/view/checkout_view.dart | 46 +++-- .../models/my_itinerary_model.dart | 52 +++-- .../repository/itinerary_repository.dart | 4 +- .../blocs/postcard_creation_bloc.dart | 14 ++ .../views/postcard_checkout_page_view.dart | 4 +- .../views/write_message_step_page_view.dart | 195 ++++++++++++++---- lib/postcard/widgets/back_card_widget.dart | 77 ++++++- 9 files changed, 318 insertions(+), 84 deletions(-) diff --git a/lib/buy_a_pass/repository/buy_pass_repository.dart b/lib/buy_a_pass/repository/buy_pass_repository.dart index 643804b..1144608 100644 --- a/lib/buy_a_pass/repository/buy_pass_repository.dart +++ b/lib/buy_a_pass/repository/buy_pass_repository.dart @@ -27,10 +27,11 @@ class BuyPassRepository { required int totalChild, required int noOfAttractions, required int noOfDays, + required double baseAmount, }) async { try { final response = await _apiService.postApi( - url: ApiUrls.addToCartPasses, // add this key in ApiUrls + url: ApiUrls.addToCartPasses, data: { "cityXid": cityXid, "cardTypeXid": cardTypeXid, @@ -38,6 +39,8 @@ class BuyPassRepository { "cardMode": cardMode, "totalAdult": totalAdult, "totalChild": totalChild, + "baseAmount": baseAmount, + "taxAmount": 2, // Fixed tax amount "noOfAttractions": noOfAttractions, "noOfDays": noOfDays, }, @@ -48,4 +51,4 @@ class BuyPassRepository { throw Exception('Failed to add passes to cart: $e'); } } -} +} \ No newline at end of file diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart index e2e2036..9fc53ae 100644 --- a/lib/buy_a_pass/widget/payment_card_view.dart +++ b/lib/buy_a_pass/widget/payment_card_view.dart @@ -181,11 +181,12 @@ class PaymentCard extends StatelessWidget { cityXid: cityXid, cardTypeXid: cardTypeXid, cardXid: cardXid, - cardMode: isSelectivePass ? 'flexi' : 'fixed', + cardMode: isSelectivePass ? 'flexi' : 'unlimited', totalAdult: adults, totalChild: children, noOfAttractions: isSelectivePass ? selectedValue : 0, noOfDays: isUnlimitedCard ? selectedValue : 0, + baseAmount: totalPrice, ); // ✅ Extract bookingId from response diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index 2359911..9aa119e 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -116,11 +116,11 @@ class _CheckoutContent extends StatelessWidget { /// 🆕 Handle payment flow with client secret /// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION - Future _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId) async { + Future _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async { final paymentSuccess = await StripePaymentScreen.showAsBottomSheet( context: context, clientSecret: clientSecret, - amount: checkoutData.totalPrice.toDouble(), + amount: finalTotal, currencySymbol: '\$', title: 'Complete Payment', loadingMessage: 'Processing your pass payment...', @@ -149,6 +149,7 @@ class _CheckoutContent extends StatelessWidget { ); }, onPaymentCancelled: () { + Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Payment cancelled'), @@ -182,22 +183,31 @@ class _CheckoutContent extends StatelessWidget { if (state is CheckoutCouponsLoadedState) { // Check if clientSecret is available (payment initiated) if (state.clientSecret != null && state.clientSecret!.isNotEmpty) { - // Trigger payment flow + // ✅ Calculate finalTotal here + double discountPercentage = 0.0; + if (state.appliedCoupon != null) { + discountPercentage = state.appliedCoupon!.discountPercent.toDouble(); + } + + final num subtotal = checkoutData.totalPrice; + final double discountAmount = subtotal * (discountPercentage / 100); + final double totalBeforeTax = subtotal - discountAmount; + final double taxAmount = 2; + final double finalTotal = totalBeforeTax + taxAmount; + + // ✅ Trigger payment flow with finalTotal WidgetsBinding.instance.addPostFrameCallback((_) { - _handlePaymentFlow(context, state.clientSecret!, state.bookingId ?? bookingId); + _handlePaymentFlow( + context, + state.clientSecret!, + state.bookingId ?? bookingId, + finalTotal, // ✅ Pass the calculated finalTotal + ); }); } // 🆕 Listen for payment confirmation success if (state.isPaymentConfirmed) { - // Show success message - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar( - // content: Text('Payment confirmed successfully!'), - // backgroundColor: Colors.green, - // ), - // ); - // Navigate to success page or back Future.delayed(const Duration(seconds: 2), () { if (context.mounted) { @@ -255,9 +265,10 @@ class _CheckoutContent extends StatelessWidget { final num subtotal = checkoutData.totalPrice; final double discountAmount = subtotal * (discountPercentage / 100); - final double taxRate = 0.05; // 5% tax + // final double taxRate = 0.05; // 5% tax final double totalBeforeTax = subtotal - discountAmount; - final double taxAmount = totalBeforeTax * taxRate; + // final double taxAmount = totalBeforeTax * taxRate; + final double taxAmount = 2; final double finalTotal = totalBeforeTax + taxAmount; return Scaffold( @@ -531,11 +542,18 @@ class _CheckoutContent extends StatelessWidget { ), builder: (_) => AllCouponsBottomsheet( onCouponSelected: (selectedCoupon) { + final coupon = selectedCoupon as AllCouponsModel; // Apply the selected coupon context.read().add( ApplyCouponEvent( coupon: selectedCoupon), ); + context.read().add( + ApplyCouponToBackendEvent( + bookingId: bookingId, + couponCode: coupon.couponCode, + ), + ); }, ), ); diff --git a/lib/itinerary_creation/models/my_itinerary_model.dart b/lib/itinerary_creation/models/my_itinerary_model.dart index 3b202ea..a2e2c87 100644 --- a/lib/itinerary_creation/models/my_itinerary_model.dart +++ b/lib/itinerary_creation/models/my_itinerary_model.dart @@ -1,18 +1,29 @@ class MyItineraryResponse { + bool isUnlimitedPass; List itineraries; - MyItineraryResponse({required this.itineraries}); + MyItineraryResponse({ + required this.isUnlimitedPass, + required this.itineraries, + }); + + factory MyItineraryResponse.fromJson(Map? json) { + json ??= {}; - factory MyItineraryResponse.fromJson(List? json) { return MyItineraryResponse( - itineraries: json == null + isUnlimitedPass: json['isUnlimitedPass'] ?? false, + itineraries: json['itineraries'] == null ? [] - : json.map((e) => MyItinerary.fromJson(e)).toList(), + : List>.from(json['itineraries']) + .map((e) => MyItinerary.fromJson(e)) + .toList(), ); } - List> toJson() => - itineraries.map((e) => e.toJson()).toList(); + Map toJson() => { + "isUnlimitedPass": isUnlimitedPass, + "itineraries": itineraries.map((e) => e.toJson()).toList(), + }; } class MyItinerary { @@ -61,22 +72,21 @@ class MyItinerary { id: (json['id'] as num?)?.toInt() ?? 0, userXid: (json['userXid'] as num?)?.toInt() ?? 0, cityXid: (json['cityXid'] as num?)?.toInt() ?? 0, - address: json['Address'] ?? "", + address: json['Address']?.toString() ?? "", latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0, longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0, - tripEnergy: json['tripEnergy'] ?? "", + tripEnergy: json['tripEnergy']?.toString() ?? "", travelingWithKids: json['travelingWithKids'] ?? false, dietaryPreferences: json['dietaryPreferences'] == null ? [] : List.from(json['dietaryPreferences']), - preferences: - Preferences.fromJson(json['preferences'] ?? {}), + preferences: Preferences.fromJson(json['preferences']), totalDays: (json['totalDays'] as num?)?.toInt() ?? 0, - aiModel: json['aiModel'] ?? "", - promptVersion: json['promptVersion'] ?? "", + aiModel: json['aiModel']?.toString() ?? "", + promptVersion: json['promptVersion']?.toString() ?? "", isActive: json['isActive'] ?? false, - createdAt: json['createdAt'] ?? "", - updatedAt: json['updatedAt'] ?? "", + createdAt: json['createdAt']?.toString() ?? "", + updatedAt: json['updatedAt']?.toString() ?? "", days: json['days'] == null ? [] : List>.from(json['days']) @@ -166,8 +176,8 @@ class ItineraryDay { id: (json['id'] as num?)?.toInt() ?? 0, itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0, dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0, - title: json['title'] ?? "", - summary: json['summary'] ?? "", + title: json['title']?.toString() ?? "", + summary: json['summary']?.toString() ?? "", items: json['items'] == null ? [] : List>.from(json['items']) @@ -216,11 +226,11 @@ class DayItem { id: (json['id'] as num?)?.toInt() ?? 0, itineraryDayXid: (json['itineraryDayXid'] as num?)?.toInt() ?? 0, - timeSlot: json['timeSlot'] ?? "", - title: json['title'] ?? "", - description: json['description'] ?? "", - locationName: json['locationName'] ?? "", - imageUrl: json['imageUrl'] ?? "", + timeSlot: json['timeSlot']?.toString() ?? "", + title: json['title']?.toString() ?? "", + description: json['description']?.toString() ?? "", + locationName: json['locationName']?.toString() ?? "", + imageUrl: json['imageUrl']?.toString() ?? "", latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0, longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0, ); diff --git a/lib/itinerary_creation/repository/itinerary_repository.dart b/lib/itinerary_creation/repository/itinerary_repository.dart index 4993c7e..aa44efa 100644 --- a/lib/itinerary_creation/repository/itinerary_repository.dart +++ b/lib/itinerary_creation/repository/itinerary_repository.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart'; import 'package:dio/dio.dart'; +import '../../localPreference/local_preference.dart'; import '../../networkApiServices/api_urls.dart'; import '../../networkApiServices/network_api_services.dart'; import '../models/my_itinerary_model.dart'; @@ -11,8 +12,9 @@ class ItineraryRepository { final NetworkApiService _apiService = NetworkApiService(); Future fetchMyItineraries() async { + final int cityId = await LocalPreference.getSelectedCityId(); final response = await _apiService.getApi( - url: ApiUrls.myItineraries, // 👈 Make sure this endpoint exists + url: '${ApiUrls.myItineraries}/$cityId', // 👈 Make sure this endpoint exists ); /// Because API returns LIST diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index 35c9d41..d7598f9 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -248,4 +248,18 @@ class PostcardCreationBloc emit(state.copyWith(isGift: event.isGift)); }); } + + // Add this getter method in PostcardCreationBloc class + String getFormattedMessage() { + if (state.message == null || state.message!.isEmpty) { + return ''; + } + + if (state.selectedFont == null || state.selectedFont!.isEmpty) { + // Default font (Poppins) + return '${state.message}'; + } + + return '${state.message}'; + } } \ No newline at end of file diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart index 8a0aab4..443cb12 100644 --- a/lib/postcard/views/postcard_checkout_page_view.dart +++ b/lib/postcard/views/postcard_checkout_page_view.dart @@ -80,7 +80,7 @@ class _PostcardCheckoutPageViewState extends State { if (creationState.imagePath != null && creationState.imagePath!.isNotEmpty) { imageFile = File(creationState.imagePath!); } - + final postcardBloc = context.read(); context.read().add( UpdateCheckoutDataEvent( countryName: widget.countryName, @@ -90,7 +90,7 @@ class _PostcardCheckoutPageViewState extends State { address1: creationState.address, address2: widget.address2, pcTitle: widget.pcTitle, - pcContent: creationState.message ?? '', + pcContent: postcardBloc.getFormattedMessage(), pcImageFile: imageFile, pcNumber: widget.pcNumber, pcDatetime: widget.pcDatetime, diff --git a/lib/postcard/views/write_message_step_page_view.dart b/lib/postcard/views/write_message_step_page_view.dart index f06ca9a..47656aa 100644 --- a/lib/postcard/views/write_message_step_page_view.dart +++ b/lib/postcard/views/write_message_step_page_view.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -8,6 +10,7 @@ import '../blocs/postcard_creation_events.dart'; import '../blocs/postcard_creation_state.dart'; import '../widgets/postcard_preview_widget.dart'; import '../widgets/step_progressbar.dart'; +import 'dart:ui' as ui; class WriteMessageStepPageView extends StatefulWidget { const WriteMessageStepPageView({super.key}); @@ -45,10 +48,10 @@ class _WriteMessageStepPageViewState extends State { TextPosition(offset: _controller.text.length)); final fonts = [ - {"name": "Default", "font": GoogleFonts.poppins()}, - {"name": "Classic", "font": GoogleFonts.playfairDisplay()}, - {"name": "Handwriting", "font": GoogleFonts.dancingScript()}, - {"name": "Elegant", "font": GoogleFonts.cormorantGaramond()}, + {"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"}, + {"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"}, + {"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"}, + {"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"}, ]; return SafeArea( @@ -91,12 +94,7 @@ class _WriteMessageStepPageViewState extends State { maxLines: 8, maxLength: 400, cursorColor: const Color(0xffF95F62), - style: TextStyle( - fontFamily: state.selectedFont ?? - GoogleFonts.poppins().fontFamily, - fontSize: 14.sp, - color: Colors.black, - ), + style: _getTextFieldStyle(state.selectedFont, fonts), decoration: InputDecoration( border: InputBorder.none, hintText: "Add Your Message Here", @@ -133,43 +131,52 @@ class _WriteMessageStepPageViewState extends State { children: fonts.map((font) { final TextStyle fontStyle = font['font'] as TextStyle; final String fontName = font["name"] as String; - final isSelected = state.selectedFont == - fontStyle.fontFamily || - (state.selectedFont == null && - fontName == "Default"); + final String cleanName = font["cleanName"] as String; + + final isSelected = state.selectedFont == cleanName || + (state.selectedFont == null && fontName == "Default"); return GestureDetector( - onTap: () => bloc - .add(ChangeFontStyle(fontStyle.fontFamily ?? "")), + onTap: () => bloc.add(ChangeFontStyle(cleanName)), child: Container( - margin: const EdgeInsets.only(right: 12), - padding: const EdgeInsets.all(12), - width: 90.w, + padding: const EdgeInsets.all(6), + width: 100.w, + height: 100.h, decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all( + ), + child: CustomPaint( + painter: DottedBorderPainter( color: isSelected ? const Color(0xffF95F62) : const Color(0xffE0E0E0), - width: 1.5, + strokeWidth: 1.5, + dashWidth: 4, + dashSpace: 3, + borderRadius: 12, + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Aa", + style: fontStyle.copyWith( + fontSize: 24.sp, + color: const Color(0xff1A1A1A), + )), + const SizedBox(height: 4), + Text(fontName, + textAlign: TextAlign.center, + style: fontStyle.copyWith( + fontSize: 11.sp, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + )), + ], + ), ), - ), - child: Column( - children: [ - Text("Aa", - style: fontStyle.copyWith( - fontSize: 20.sp, - color: const Color(0xff1A1A1A), - )), - const SizedBox(height: 4), - Text(fontName, - style: TextStyle( - fontSize: 12.sp, - color: isSelected - ? const Color(0xffF95F62) - : const Color(0xff2D3134), - )), - ], ), ), ); @@ -209,5 +216,117 @@ class _WriteMessageStepPageViewState extends State { }, ); } + + // Helper method to get the correct font style for the text field + TextStyle _getTextFieldStyle(String? selectedFont, List> fonts) { + if (selectedFont == null || selectedFont.isEmpty) { + return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); + } + + // Find matching font by cleanName + for (var font in fonts) { + if (font['cleanName'] == selectedFont) { + final TextStyle fontStyle = font['font'] as TextStyle; + return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black); + } + } + + // Default fallback to Poppins + return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); + } } +// Custom Painter for Dotted Border +class DottedBorderPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double dashWidth; + final double dashSpace; + final double borderRadius; + + DottedBorderPainter({ + required this.color, + this.strokeWidth = 1.5, + this.dashWidth = 4, + this.dashSpace = 3, + this.borderRadius = 12, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; + + final path = Path() + ..addRRect(RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + Radius.circular(borderRadius), + )); + + // Create dashed path + final dashPath = _createDashedPath(path, dashWidth, dashSpace); + canvas.drawPath(dashPath, paint); + } + + Path _createDashedPath(Path source, double dashWidth, double dashSpace) { + final Path dest = Path(); + for (final PathMetric metric in source.computeMetrics()) { + double distance = 0.0; + bool draw = true; + while (distance < metric.length) { + final double length = draw ? dashWidth : dashSpace; + if (distance + length > metric.length) { + if (draw) { + dest.addPath( + metric.extractPath(distance, metric.length), + Offset.zero, + ); + } + break; + } else { + if (draw) { + dest.addPath( + metric.extractPath(distance, distance + length), + Offset.zero, + ); + } + distance += length; + draw = !draw; + } + } + } + return dest; + } + + @override + bool shouldRepaint(DottedBorderPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth || + oldDelegate.dashWidth != dashWidth || + oldDelegate.dashSpace != dashSpace; + } +} + +// Lined Paper Painter (assuming this exists in your original code) +class LinedPaperPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xFFE0E0E0) + ..strokeWidth = 1; + + const lineSpacing = 30.0; + for (double i = lineSpacing; i < size.height; i += lineSpacing) { + canvas.drawLine( + Offset(0, i), + Offset(size.width, i), + paint, + ); + } + } + + @override + bool shouldRepaint(LinedPaperPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/lib/postcard/widgets/back_card_widget.dart b/lib/postcard/widgets/back_card_widget.dart index a05c97f..2009c15 100644 --- a/lib/postcard/widgets/back_card_widget.dart +++ b/lib/postcard/widgets/back_card_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:html/parser.dart' as html_parser; class BackCardWidget extends StatelessWidget { final String message; @@ -26,8 +27,77 @@ class BackCardWidget extends StatelessWidget { this.scale = 1.08, }); + // Parse HTML message and extract font family and text + Map _parseHtmlMessage(String htmlMessage) { + if (htmlMessage.isEmpty) { + return {'text': '', 'fontFamily': ''}; + } + + // Check if message contains HTML tags + if (!htmlMessage.contains(' Date: Tue, 10 Feb 2026 15:05:38 +0530 Subject: [PATCH 4/8] itnerary --- .../current_location_selection.dart | 117 +++++++++++------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart b/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart index e4e685d..2fa8d6e 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/current_location_selection.dart @@ -23,30 +23,47 @@ class CurrentLocationSelection extends StatefulWidget { class _CurrentLocationSelectionState extends State { final TextEditingController _controller = TextEditingController(); LatLng? _currentLatLng; + bool loading = false; Future _getCurrentLocation() async { - LocationPermission permission = await Geolocator.requestPermission(); + try { + setState(() { + loading = true; + }); + LocationPermission permission = await Geolocator.requestPermission(); - if (permission == LocationPermission.denied || - permission == LocationPermission.deniedForever) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Location permission denied')), + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Location permission denied')), + ); + return; + } + + final position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, ); - return; + + final lat = position.latitude; + final lng = position.longitude; + + setState(() { + _currentLatLng = LatLng(lat, lng); + }); + + await _getAddressFromLatLng(lat, lng); + setState(() { + loading = false; + }); + } catch (e) { + setState(() { + loading = false; + }); + } finally { + setState(() { + loading = false; + }); } - - final position = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.high, - ); - - final lat = position.latitude; - final lng = position.longitude; - - setState(() { - _currentLatLng = LatLng(lat, lng); - }); - - await _getAddressFromLatLng(lat, lng); } Future _getAddressFromLatLng(double lat, double lng) async { @@ -57,7 +74,6 @@ class _CurrentLocationSelectionState extends State { final place = placemarks.first; final address = [ - place.name, place.street, place.subLocality, place.locality, @@ -133,34 +149,41 @@ class _CurrentLocationSelectionState extends State { child: SizedBox( height: 250.h, width: double.infinity, - child: FlutterMap( - options: MapOptions( - initialCenter: _currentLatLng!, - initialZoom: 15, - ), - children: [ - TileLayer( - urlTemplate: - "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - subdomains: const ['a', 'b', 'c'], - userAgentPackageName: 'com.citycards.customer', - ), - MarkerLayer( - markers: [ - Marker( - point: _currentLatLng!, - width: 40, - height: 40, - child: const Icon( - Icons.location_pin, - color: Colors.red, - size: 40, - ), + child: loading == true + ? Center( + child: CircularProgressIndicator( + color: Color(0xFFF95F62), ), - ], - ), - ], - ), + ) + : FlutterMap( + options: MapOptions( + initialCenter: _currentLatLng!, + initialZoom: 15, + ), + children: [ + TileLayer( + urlTemplate: + "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + subdomains: const ['a', 'b', 'c'], + userAgentPackageName: + 'com.citycards.customer', + ), + MarkerLayer( + markers: [ + Marker( + point: _currentLatLng!, + width: 40, + height: 40, + child: const Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + ], + ), ), ) : GestureDetector( From 5d08e07de3f1dd909d7e890b1b1aae6952f692bb Mon Sep 17 00:00:00 2001 From: mystery012728 Date: Tue, 10 Feb 2026 19:05:42 +0530 Subject: [PATCH 5/8] added pass details screen new and updated create account page and more changes... --- assets/images/unlimited_card_details.png | Bin 0 -> 53995 bytes lib/attractions/widget/attraction_card.dart | 2 +- lib/buy_a_pass/view/buy_pass_view.dart | 8 +- .../custom_dash_border_painter.dart | 49 ++ lib/core/app_router.dart | 27 +- lib/core/inside_bottom_navigator.dart | 41 +- lib/core/route_constants.dart | 3 + .../bloc/create_account_bloc.dart | 17 +- .../bloc/create_account_event.dart | 14 +- .../repository/create_account_repository.dart | 22 +- .../view/create_account_view.dart | 72 ++- .../bloc/get_itinerary_bloc.dart | 17 +- .../bloc/get_itinerary_state.dart | 9 + .../views/magic_itinerary_view.dart | 96 ++- lib/login/view/verify_otp_bottomsheet.dart | 2 +- .../views/pass_attraction_details_view.dart | 594 ++++++++++++++++++ .../views/pass_attractions_page_view.dart | 160 +++++ lib/my_pass/views/pass_details_page_view.dart | 460 ++++++++++++++ lib/my_pass/views/qr_pass_page_view.dart | 144 ----- .../search_pass_offers_with_listing.dart | 348 ++++++++++ lib/my_pass/widgets/pass_attraction_card.dart | 191 ++++++ lib/profile/bloc/profile/profile_bloc.dart | 7 +- lib/profile/bloc/profile/profile_event.dart | 17 + .../repository/profile_repository.dart | 83 ++- .../view/edit_profile/edit_profile_view.dart | 67 +- .../view/search_offers_with_listing.dart | 12 +- 26 files changed, 2220 insertions(+), 242 deletions(-) create mode 100644 assets/images/unlimited_card_details.png create mode 100644 lib/common_packages/custom_dash_border_painter.dart create mode 100644 lib/my_pass/views/pass_attraction_details_view.dart create mode 100644 lib/my_pass/views/pass_attractions_page_view.dart create mode 100644 lib/my_pass/views/pass_details_page_view.dart delete mode 100644 lib/my_pass/views/qr_pass_page_view.dart create mode 100644 lib/my_pass/views/search_pass_offers_with_listing.dart create mode 100644 lib/my_pass/widgets/pass_attraction_card.dart diff --git a/assets/images/unlimited_card_details.png b/assets/images/unlimited_card_details.png new file mode 100644 index 0000000000000000000000000000000000000000..1345db4bfecdee7b8b083df7a103fba9d216f34b GIT binary patch literal 53995 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 n?v?<+nNbRA#- 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, From 53264619a844ec87ae6b2311ff145fad89feca62 Mon Sep 17 00:00:00 2001 From: Shreeyash Thorat <120039092+ShreeyashThorat@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:25:05 +0530 Subject: [PATCH 6/8] postcard edit --- lib/core/app_router.dart | 18 +- lib/core/route_constants.dart | 16 +- lib/networkApiServices/api_urls.dart | 9 +- .../network_api_services.dart | 23 +- .../edit_postcard/edit_postcard_bloc.dart | 26 ++ .../edit_postcard/edit_postcard_event.dart | 13 + .../edit_postcard/edit_postcard_state.dart | 19 + .../blocs/myPostCards/my_postcard_bloc.dart | 205 +++++---- .../blocs/myPostCards/my_postcard_event.dart | 7 +- .../blocs/myPostCards/my_postcard_state.dart | 12 +- .../postcard_checkout_bloc.dart | 248 ++++++----- lib/postcard/models/my_postcard_model.dart | 77 ++++ .../repository/my_postcard_repository.dart | 63 ++- lib/postcard/views/edit_postcard_view.dart | 376 +++++++++++++++++ .../views/my_postcard_drafts_view.dart | 388 +++++++++++------- .../views/my_postcard_orders_view.dart | 242 +++++------ .../views/write_message_step_page_view.dart | 96 +++-- .../widgets/edit_post_card/edit_message.dart | 257 ++++++++++++ .../widgets/edit_post_card/your_details.dart | 260 ++++++++++++ pubspec.lock | 8 + pubspec.yaml | 1 + 21 files changed, 1857 insertions(+), 507 deletions(-) create mode 100644 lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart create mode 100644 lib/postcard/blocs/edit_postcard/edit_postcard_event.dart create mode 100644 lib/postcard/blocs/edit_postcard/edit_postcard_state.dart create mode 100644 lib/postcard/views/edit_postcard_view.dart create mode 100644 lib/postcard/widgets/edit_post_card/edit_message.dart create mode 100644 lib/postcard/widgets/edit_post_card/your_details.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 8079eba..db31b8e 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,4 +1,3 @@ - import 'package:citycards_customer/add_details/add_details_view.dart'; import 'package:citycards_customer/attraction_details/views/attraction_details_view.dart'; import 'package:citycards_customer/attractions/models/attraction_model.dart'; @@ -153,7 +152,7 @@ class AppRouter { final attractionId = settings.arguments as Attraction; return MaterialPageRoute( builder: (_) { - return AttractionDetailsView(attractionId: attractionId.id,); + return AttractionDetailsView(attractionId: attractionId.id); }, ); @@ -168,12 +167,9 @@ class AppRouter { final bookingId = settings.arguments as int; // or String return MaterialPageRoute( - builder: (_) => CheckoutView( - bookingId: bookingId, - ), + builder: (_) => CheckoutView(bookingId: bookingId), ); - case RouteConstants.cartPage: return MaterialPageRoute( builder: (_) { @@ -196,13 +192,10 @@ class AppRouter { return MaterialPageRoute( builder: (_) { - return AddDetailsView( - bookingId: bookingId, - ); + return AddDetailsView(bookingId: bookingId); }, ); - case RouteConstants.createAcct: final email = settings.arguments as String; @@ -239,12 +232,9 @@ class AppRouter { final offerId = settings.arguments as int; return MaterialPageRoute( - builder: (_) => OffersDetailsView( - offerId: offerId, - ), + builder: (_) => OffersDetailsView(offerId: offerId), ); - case RouteConstants.registeredUserHome: return MaterialPageRoute( builder: (_) { diff --git a/lib/core/route_constants.dart b/lib/core/route_constants.dart index 0d8c270..5fb4b51 100644 --- a/lib/core/route_constants.dart +++ b/lib/core/route_constants.dart @@ -1,6 +1,4 @@ class RouteConstants { - - static const String intro = '/intro'; static const String splash = '/splash'; @@ -27,33 +25,33 @@ class RouteConstants { static const String magicItineraryEmptyScreen = '/magicItineraryEmptyScreen'; static const String itineraryCreationStart = '/itineraryCreationStart'; static const String itineraryCreation = '/itineraryCreation'; - static const String magicItineraryFilledScreen = "/magicItineraryFilledScreen"; + static const String magicItineraryFilledScreen = + "/magicItineraryFilledScreen"; /**************************** ESIM Page *****************************************/ static const String esimOffer = '/esim_offer'; static const String hotelOffer = '/hotelOffer'; -/**************************** Attraction Page *****************************************/ + /**************************** Attraction Page *****************************************/ - static const String attractionDetails ='/attractionDetails'; + static const String attractionDetails = '/attractionDetails'; /**************************** By Pass Page Page *****************************************/ - static const String buyPass ='/buyPass'; - static const String checkout ='/checkout'; + static const String buyPass = '/buyPass'; + static const String checkout = '/checkout'; static const String searchOffer = '/searchOffer'; static const String createAcct = '/createAcct'; static const String addDetails = '/addDetails'; static const String offerPassDetail = "/offerPassDetail"; - /************************** My card page ***************************************/ static const String cartPage = '/cartPage'; static const String yourItinerary = '/yourItinerary'; - static const String qrPage = '/qrPage'; static const String makeBooking = '/makeBooking'; static const String bookingSuccessful = '/bookingSuccessful'; + static const String editPostCard = '/editPostCard'; } diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 25c2b00..67fd469 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -1,6 +1,6 @@ class ApiUrls { - - static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API + static const baseUrl = + "https://devapi.citycards.betadelivery.com"; //Normal API // static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API // static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API @@ -20,8 +20,11 @@ class ApiUrls { static const myPostCards = "$baseUrl/mobile/postcards/all"; static const coupons = "$baseUrl/mobile/passes/dropdown/card"; + static const editPostcard = "$baseUrl/mobile/postcards"; + static const myItineraries = "$baseUrl/mobile/itinerary/all-initineraries"; - static const getItineraryCities = "$baseUrl/mobile/itinerary/cities-with-icons"; + static const getItineraryCities = + "$baseUrl/mobile/itinerary/cities-with-icons"; //Post Apis static const createAccount = "$baseUrl/mobile/user/register"; diff --git a/lib/networkApiServices/network_api_services.dart b/lib/networkApiServices/network_api_services.dart index 8e5ef09..73d79da 100644 --- a/lib/networkApiServices/network_api_services.dart +++ b/lib/networkApiServices/network_api_services.dart @@ -185,6 +185,27 @@ class NetworkApiService { } } + // ================= DELETE ================= + Future deleteApi({ + required String url, + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.delete( + url, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } on DioException catch (e) { + throw _handleError(e); + } + } + // ================= REFRESH TOKEN ================= Future _refreshToken() async { try { @@ -225,7 +246,7 @@ class NetworkApiService { case DioExceptionType.badCertificate: return "Bad certificate."; case DioExceptionType.badResponse: - // 🔥 FIXED: Safely handle different response data types + // 🔥 FIXED: Safely handle different response data types try { final responseData = error.response?.data; diff --git a/lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart b/lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart new file mode 100644 index 0000000..5303700 --- /dev/null +++ b/lib/postcard/blocs/edit_postcard/edit_postcard_bloc.dart @@ -0,0 +1,26 @@ +import 'dart:developer'; + +import 'package:bloc/bloc.dart'; +import 'package:citycards_customer/postcard/models/my_postcard_model.dart'; +import 'package:citycards_customer/postcard/repository/my_postcard_repository.dart'; +import 'package:equatable/equatable.dart'; + +part 'edit_postcard_event.dart'; +part 'edit_postcard_state.dart'; + +class EditPostcardBloc extends Bloc { + EditPostcardBloc() : super(EditPostcardInitial()) { + on((event, emit) async { + try { + emit(EditPostcardLoading()); + await MyPostCardsRepository().editMyPostCards( + postcard: event.myPostCard, + ); + log("Edit PostCard Successfully"); + emit(EditPostcardSuccessfull()); + } catch (e) { + emit(EditPostcardError(error: "Failed to edit postcard")); + } + }); + } +} diff --git a/lib/postcard/blocs/edit_postcard/edit_postcard_event.dart b/lib/postcard/blocs/edit_postcard/edit_postcard_event.dart new file mode 100644 index 0000000..61b14be --- /dev/null +++ b/lib/postcard/blocs/edit_postcard/edit_postcard_event.dart @@ -0,0 +1,13 @@ +part of 'edit_postcard_bloc.dart'; + +class EditPostcardEvent extends Equatable { + const EditPostcardEvent(); + + @override + List get props => []; +} + +class EditPostCard extends EditPostcardEvent { + final MyPostCard myPostCard; + const EditPostCard({required this.myPostCard}); +} diff --git a/lib/postcard/blocs/edit_postcard/edit_postcard_state.dart b/lib/postcard/blocs/edit_postcard/edit_postcard_state.dart new file mode 100644 index 0000000..e0c9d74 --- /dev/null +++ b/lib/postcard/blocs/edit_postcard/edit_postcard_state.dart @@ -0,0 +1,19 @@ +part of 'edit_postcard_bloc.dart'; + +class EditPostcardState extends Equatable { + const EditPostcardState(); + + @override + List get props => []; +} + +class EditPostcardInitial extends EditPostcardState {} + +class EditPostcardLoading extends EditPostcardState {} + +class EditPostcardSuccessfull extends EditPostcardState {} + +class EditPostcardError extends EditPostcardState { + final String error; + const EditPostcardError({required this.error}); +} diff --git a/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart b/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart index fa9b4d5..286a3e1 100644 --- a/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart +++ b/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart @@ -1,6 +1,9 @@ +import 'dart:developer'; + import 'package:citycards_customer/localPreference/local_preference.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:developer' as developer; +import '../../models/my_postcard_model.dart'; import '../../repository/my_postcard_repository.dart'; import 'my_postcard_event.dart'; import 'my_postcard_state.dart'; @@ -8,19 +11,21 @@ import 'my_postcard_state.dart'; class MyPostCardBloc extends Bloc { final MyPostCardsRepository repository; - MyPostCardBloc({required this.repository}) : super(const MyPostCardInitial()) { + MyPostCardBloc({required this.repository}) + : super(const MyPostCardInitial()) { on(_onCheckLoginStatus); on(_onFetchDraftPostCards); on(_onFetchOrderPostCards); on(_onRefreshDraftPostCards); on(_onRefreshOrderPostCards); + on(_onDeletePostCard); } /// Handle checking login status Future _onCheckLoginStatus( - CheckLoginStatus event, - Emitter emit, - ) async { + CheckLoginStatus event, + Emitter emit, + ) async { developer.log('🔍 Checking login status...', name: 'MyPostCardBloc'); emit(const MyPostCardCheckingLogin()); @@ -29,20 +34,28 @@ class MyPostCardBloc extends Bloc { developer.log('📊 Login status: $isLogin', name: 'MyPostCardBloc'); if (isLogin) { - developer.log('✅ User is logged in - initializing state', name: 'MyPostCardBloc'); + developer.log( + '✅ User is logged in - initializing state', + name: 'MyPostCardBloc', + ); // User is logged in, initialize with empty lists and loading states - emit(const MyPostCardLoaded( - draftPostCards: [], - orderPostCards: [], - isDraftLoading: true, - isOrderLoading: true, - )); + emit( + const MyPostCardLoaded( + draftPostCards: [], + orderPostCards: [], + isDraftLoading: true, + isOrderLoading: true, + ), + ); // Fetch both drafts and orders add(const FetchDraftPostCards()); add(const FetchOrderPostCards()); } else { - developer.log('❌ User is NOT logged in - emitting MyPostCardNotLoggedIn', name: 'MyPostCardBloc'); + developer.log( + '❌ User is NOT logged in - emitting MyPostCardNotLoggedIn', + name: 'MyPostCardBloc', + ); // User is not logged in emit(const MyPostCardNotLoggedIn()); } @@ -55,9 +68,9 @@ class MyPostCardBloc extends Bloc { /// Handle fetching draft postcards Future _onFetchDraftPostCards( - FetchDraftPostCards event, - Emitter emit, - ) async { + FetchDraftPostCards event, + Emitter emit, + ) async { developer.log('📥 Fetching draft postcards...', name: 'MyPostCardBloc'); // Get current state final currentState = state; @@ -69,23 +82,33 @@ class MyPostCardBloc extends Bloc { try { final draftPostCards = await repository.fetchMyPostCards(type: 'draft'); - developer.log('✅ Draft postcards fetched: ${draftPostCards.length} items', name: 'MyPostCardBloc'); + developer.log( + '✅ Draft postcards fetched: ${draftPostCards.length} items', + name: 'MyPostCardBloc', + ); if (state is MyPostCardLoaded) { // Update with fetched drafts - emit((state as MyPostCardLoaded).copyWith( - draftPostCards: draftPostCards, - isDraftLoading: false, - )); + emit( + (state as MyPostCardLoaded).copyWith( + draftPostCards: draftPostCards, + isDraftLoading: false, + ), + ); } else { - developer.log('⚠️ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc'); + developer.log( + '⚠️ State is not MyPostCardLoaded, creating new state', + name: 'MyPostCardBloc', + ); // Fallback: create new loaded state (shouldn't normally happen) - emit(MyPostCardLoaded( - draftPostCards: draftPostCards, - orderPostCards: const [], - isDraftLoading: false, - isOrderLoading: false, - )); + emit( + MyPostCardLoaded( + draftPostCards: draftPostCards, + orderPostCards: const [], + isDraftLoading: false, + isOrderLoading: false, + ), + ); } } catch (error) { developer.log('❌ Error fetching drafts: $error', name: 'MyPostCardBloc'); @@ -95,18 +118,37 @@ class MyPostCardBloc extends Bloc { } // Emit error state - emit(MyPostCardError( - errorMessage: error.toString(), - errorType: 'draft', - )); + emit(MyPostCardError(errorMessage: error.toString(), errorType: 'draft')); + } + } + + Future _onDeletePostCard( + DeleteDraftPostCards event, + Emitter emit, + ) async { + if (state is MyPostCardLoaded) { + MyPostCardLoaded currentState = state as MyPostCardLoaded; + try { + emit(currentState.copyWith(isDeleteLoading: true)); + await MyPostCardsRepository().deleteMyPostCards(event.id); + + List items = currentState.draftPostCards; + items.removeWhere((e) => e.id == event.id); + emit( + currentState.copyWith(draftPostCards: items, isDeleteLoading: false), + ); + } catch (e) { + log("Erro - $e"); + emit(currentState.copyWith(isDeleteLoading: false)); + } } } /// Handle fetching order postcards Future _onFetchOrderPostCards( - FetchOrderPostCards event, - Emitter emit, - ) async { + FetchOrderPostCards event, + Emitter emit, + ) async { developer.log('📥 Fetching order postcards...', name: 'MyPostCardBloc'); // Get current state final currentState = state; @@ -118,23 +160,33 @@ class MyPostCardBloc extends Bloc { try { final orderPostCards = await repository.fetchMyPostCards(type: 'orders'); - developer.log('✅ Order postcards fetched: ${orderPostCards.length} items', name: 'MyPostCardBloc'); + developer.log( + '✅ Order postcards fetched: ${orderPostCards.length} items', + name: 'MyPostCardBloc', + ); if (state is MyPostCardLoaded) { // Update with fetched orders - emit((state as MyPostCardLoaded).copyWith( - orderPostCards: orderPostCards, - isOrderLoading: false, - )); + emit( + (state as MyPostCardLoaded).copyWith( + orderPostCards: orderPostCards, + isOrderLoading: false, + ), + ); } else { - developer.log('⚠️ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc'); + developer.log( + '⚠️ State is not MyPostCardLoaded, creating new state', + name: 'MyPostCardBloc', + ); // Fallback: create new loaded state (shouldn't normally happen) - emit(MyPostCardLoaded( - draftPostCards: const [], - orderPostCards: orderPostCards, - isDraftLoading: false, - isOrderLoading: false, - )); + emit( + MyPostCardLoaded( + draftPostCards: const [], + orderPostCards: orderPostCards, + isDraftLoading: false, + isOrderLoading: false, + ), + ); } } catch (error) { developer.log('❌ Error fetching orders: $error', name: 'MyPostCardBloc'); @@ -144,58 +196,61 @@ class MyPostCardBloc extends Bloc { } // Emit error state - emit(MyPostCardError( - errorMessage: error.toString(), - errorType: 'order', - )); + emit(MyPostCardError(errorMessage: error.toString(), errorType: 'order')); } } /// Handle refreshing draft postcards Future _onRefreshDraftPostCards( - RefreshDraftPostCards event, - Emitter emit, - ) async { + RefreshDraftPostCards event, + Emitter emit, + ) async { developer.log('🔄 Refreshing draft postcards...', name: 'MyPostCardBloc'); try { final draftPostCards = await repository.fetchMyPostCards(type: 'draft'); - developer.log('✅ Draft postcards refreshed: ${draftPostCards.length} items', name: 'MyPostCardBloc'); + developer.log( + '✅ Draft postcards refreshed: ${draftPostCards.length} items', + name: 'MyPostCardBloc', + ); if (state is MyPostCardLoaded) { - emit((state as MyPostCardLoaded).copyWith( - draftPostCards: draftPostCards, - )); + emit( + (state as MyPostCardLoaded).copyWith(draftPostCards: draftPostCards), + ); } } catch (error) { - developer.log('❌ Error refreshing drafts: $error', name: 'MyPostCardBloc'); - emit(MyPostCardError( - errorMessage: error.toString(), - errorType: 'draft', - )); + developer.log( + '❌ Error refreshing drafts: $error', + name: 'MyPostCardBloc', + ); + emit(MyPostCardError(errorMessage: error.toString(), errorType: 'draft')); } } /// Handle refreshing order postcards Future _onRefreshOrderPostCards( - RefreshOrderPostCards event, - Emitter emit, - ) async { + RefreshOrderPostCards event, + Emitter emit, + ) async { developer.log('🔄 Refreshing order postcards...', name: 'MyPostCardBloc'); try { final orderPostCards = await repository.fetchMyPostCards(type: 'orders'); - developer.log('✅ Order postcards refreshed: ${orderPostCards.length} items', name: 'MyPostCardBloc'); + developer.log( + '✅ Order postcards refreshed: ${orderPostCards.length} items', + name: 'MyPostCardBloc', + ); if (state is MyPostCardLoaded) { - emit((state as MyPostCardLoaded).copyWith( - orderPostCards: orderPostCards, - )); + emit( + (state as MyPostCardLoaded).copyWith(orderPostCards: orderPostCards), + ); } } catch (error) { - developer.log('❌ Error refreshing orders: $error', name: 'MyPostCardBloc'); - emit(MyPostCardError( - errorMessage: error.toString(), - errorType: 'order', - )); + developer.log( + '❌ Error refreshing orders: $error', + name: 'MyPostCardBloc', + ); + emit(MyPostCardError(errorMessage: error.toString(), errorType: 'order')); } } -} \ No newline at end of file +} diff --git a/lib/postcard/blocs/myPostCards/my_postcard_event.dart b/lib/postcard/blocs/myPostCards/my_postcard_event.dart index 7c25209..4e931cc 100644 --- a/lib/postcard/blocs/myPostCards/my_postcard_event.dart +++ b/lib/postcard/blocs/myPostCards/my_postcard_event.dart @@ -17,6 +17,11 @@ class FetchDraftPostCards extends MyPostCardEvent { const FetchDraftPostCards(); } +class DeleteDraftPostCards extends MyPostCardEvent { + final int id; + const DeleteDraftPostCards({required this.id}); +} + /// Event to fetch order postcards class FetchOrderPostCards extends MyPostCardEvent { const FetchOrderPostCards(); @@ -30,4 +35,4 @@ class RefreshDraftPostCards extends MyPostCardEvent { /// Event to refresh order postcards class RefreshOrderPostCards extends MyPostCardEvent { const RefreshOrderPostCards(); -} \ No newline at end of file +} diff --git a/lib/postcard/blocs/myPostCards/my_postcard_state.dart b/lib/postcard/blocs/myPostCards/my_postcard_state.dart index cef9dd7..8b1e6b7 100644 --- a/lib/postcard/blocs/myPostCards/my_postcard_state.dart +++ b/lib/postcard/blocs/myPostCards/my_postcard_state.dart @@ -29,12 +29,14 @@ class MyPostCardLoaded extends MyPostCardState { final List orderPostCards; final bool isDraftLoading; final bool isOrderLoading; + final bool isDeleteLoading; const MyPostCardLoaded({ required this.draftPostCards, required this.orderPostCards, this.isDraftLoading = false, this.isOrderLoading = false, + this.isDeleteLoading = false, }); @override @@ -43,6 +45,7 @@ class MyPostCardLoaded extends MyPostCardState { orderPostCards, isDraftLoading, isOrderLoading, + isDeleteLoading, ]; /// Helper method to create a copy with updated values @@ -51,12 +54,14 @@ class MyPostCardLoaded extends MyPostCardState { List? orderPostCards, bool? isDraftLoading, bool? isOrderLoading, + bool? isDeleteLoading, }) { return MyPostCardLoaded( draftPostCards: draftPostCards ?? this.draftPostCards, orderPostCards: orderPostCards ?? this.orderPostCards, isDraftLoading: isDraftLoading ?? this.isDraftLoading, isOrderLoading: isOrderLoading ?? this.isOrderLoading, + isDeleteLoading: isDeleteLoading ?? this.isDeleteLoading, ); } } @@ -66,11 +71,8 @@ class MyPostCardError extends MyPostCardState { final String errorMessage; final String errorType; // 'draft' or 'order' - const MyPostCardError({ - required this.errorMessage, - required this.errorType, - }); + const MyPostCardError({required this.errorMessage, required this.errorType}); @override List get props => [errorMessage, errorType]; -} \ No newline at end of file +} diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart index 016b511..650df24 100644 --- a/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart +++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter_bloc/flutter_bloc.dart'; import '../../repository/postcard_checkout_repository.dart'; import 'postcard_checkout_event.dart'; @@ -8,7 +10,7 @@ class PostcardCheckoutBloc final CreatePostCardRepository repository; PostcardCheckoutBloc({required this.repository}) - : super(const PostcardCheckoutState()) { + : super(const PostcardCheckoutState()) { on(_onUpdateAddress); on(_onUpdateContent); on(_onUpdateCheckoutData); @@ -18,63 +20,79 @@ class PostcardCheckoutBloc } void _onUpdateAddress( - UpdateAddressEvent event, Emitter emit) { - emit(state.copyWith( - countryName: event.countryName, - cityName: event.cityName, - stateName: event.stateName, - zipCode: event.zipCode, - address1: event.address1, - address2: event.address2, - )); + UpdateAddressEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + countryName: event.countryName, + cityName: event.cityName, + stateName: event.stateName, + zipCode: event.zipCode, + address1: event.address1, + address2: event.address2, + ), + ); } void _onUpdateContent( - UpdatePostcardContentEvent event, Emitter emit) { - emit(state.copyWith( - pcTitle: event.pcTitle, - pcContent: event.pcContent, - pcImageFile: event.pcImageFile, - )); + UpdatePostcardContentEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + pcTitle: event.pcTitle, + pcContent: event.pcContent, + pcImageFile: event.pcImageFile, + ), + ); } void _onUpdateCheckoutData( - UpdateCheckoutDataEvent event, Emitter emit) { - emit(state.copyWith( - countryName: event.countryName, - cityName: event.cityName, - stateName: event.stateName, - zipCode: event.zipCode, - address1: event.address1, - address2: event.address2, - pcTitle: event.pcTitle, - pcContent: event.pcContent, - pcImageFile: event.pcImageFile, - pcNumber: event.pcNumber, - pcDatetime: event.pcDatetime, - fullname: event.fullname, - emailAddress: event.emailAddress, - mobileNumber: event.mobileNumber, - isdCode: event.isdCode, - isForSelf: event.isForSelf, - baseAmount: event.baseAmount, - totalTaxAmount: event.totalTaxAmount, - totalAmount: event.totalAmount, - )); + UpdateCheckoutDataEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + countryName: event.countryName, + cityName: event.cityName, + stateName: event.stateName, + zipCode: event.zipCode, + address1: event.address1, + address2: event.address2, + pcTitle: event.pcTitle, + pcContent: event.pcContent, + pcImageFile: event.pcImageFile, + pcNumber: event.pcNumber, + pcDatetime: event.pcDatetime, + fullname: event.fullname, + emailAddress: event.emailAddress, + mobileNumber: event.mobileNumber, + isdCode: event.isdCode, + isForSelf: event.isForSelf, + baseAmount: event.baseAmount, + totalTaxAmount: event.totalTaxAmount, + totalAmount: event.totalAmount, + ), + ); } Future _onSaveAsDraft( - SaveAsDraftEvent event, Emitter emit) async { + SaveAsDraftEvent event, + Emitter emit, + ) async { emit(state.copyWith(isLoading: true, error: null, isSuccess: false)); try { // Validate that image file exists before submitting if (state.pcImageFile == null) { - emit(state.copyWith( - isLoading: false, - error: 'Please select a postcard image', - isSuccess: false, - )); + emit( + state.copyWith( + isLoading: false, + error: 'Please select a postcard image', + isSuccess: false, + ), + ); return; } @@ -102,37 +120,42 @@ class PostcardCheckoutBloc ); // Extract order ID from response if available - final orderId = response['orderId']?.toString() ?? + final orderId = + response['orderId']?.toString() ?? response['order_id']?.toString() ?? response['id']?.toString(); - emit(state.copyWith( - isLoading: false, - isSuccess: true, - isDraft: true, - orderId: orderId, - )); + emit( + state.copyWith( + isLoading: false, + isSuccess: true, + isDraft: true, + orderId: orderId, + ), + ); } catch (e) { - emit(state.copyWith( - isLoading: false, - error: e.toString(), - isSuccess: false, - )); + emit( + state.copyWith(isLoading: false, error: e.toString(), isSuccess: false), + ); } } Future _onSubmitPostcard( - SubmitPostcardEvent event, Emitter emit) async { + SubmitPostcardEvent event, + Emitter emit, + ) async { emit(state.copyWith(isLoading: true, error: null, isSuccess: false)); try { // Validate that image file exists before submitting if (state.pcImageFile == null) { - emit(state.copyWith( - isLoading: false, - error: 'Please select a postcard image', - isSuccess: false, - )); + emit( + state.copyWith( + isLoading: false, + error: 'Please select a postcard image', + isSuccess: false, + ), + ); return; } @@ -166,58 +189,69 @@ class PostcardCheckoutBloc final clientSecret = response['clientSecret'] as String?; // Also try alternative key names in case backend uses different naming - final orderId = response['orderId']?.toString() ?? + final orderId = + response['orderId']?.toString() ?? response['order_id']?.toString() ?? response['id']?.toString(); // Validate clientSecret is present if (clientSecret == null || clientSecret.isEmpty) { - emit(state.copyWith( - isLoading: false, - error: 'Payment initialization failed - no client secret received from server', - isSuccess: false, - )); + emit( + state.copyWith( + isLoading: false, + error: + 'Payment initialization failed - no client secret received from server', + isSuccess: false, + ), + ); return; } // 🆕 Emit success with clientSecret for payment processing - emit(state.copyWith( - isLoading: false, - isSuccess: true, - isDraft: false, - postcardId: postcardId, - clientSecret: clientSecret, // This will trigger payment flow - orderId: orderId, - )); - } catch (e) { - emit(state.copyWith( - isLoading: false, - error: e.toString(), - isSuccess: false, - )); + emit( + state.copyWith( + isLoading: false, + isSuccess: true, + isDraft: false, + postcardId: postcardId, + clientSecret: clientSecret, // This will trigger payment flow + orderId: orderId, + ), + ); + } catch (e, stack) { + log("Payment Error ${e.toString()}"); + log("Payment Error ${stack.toString()}"); + emit( + state.copyWith(isLoading: false, error: e.toString(), isSuccess: false), + ); } } /// 🆕 Confirm payment after Stripe payment completes /// This should be called after Stripe payment succeeds or fails Future _onConfirmPayment( - ConfirmPaymentEvent event, Emitter emit) async { - + ConfirmPaymentEvent event, + Emitter emit, + ) async { // Validate postcardId exists if (state.postcardId == null) { - emit(state.copyWith( - confirmationError: 'Cannot confirm payment - postcard ID is missing', - isConfirmingPayment: false, - isPaymentConfirmed: false, - )); + emit( + state.copyWith( + confirmationError: 'Cannot confirm payment - postcard ID is missing', + isConfirmingPayment: false, + isPaymentConfirmed: false, + ), + ); return; } - emit(state.copyWith( - isConfirmingPayment: true, - confirmationError: null, - isPaymentConfirmed: false, - )); + emit( + state.copyWith( + isConfirmingPayment: true, + confirmationError: null, + isPaymentConfirmed: false, + ), + ); try { final response = await repository.confirmPayment( @@ -227,17 +261,21 @@ class PostcardCheckoutBloc ); // Payment confirmation successful - emit(state.copyWith( - isConfirmingPayment: false, - isPaymentConfirmed: true, - confirmationError: null, - )); + emit( + state.copyWith( + isConfirmingPayment: false, + isPaymentConfirmed: true, + confirmationError: null, + ), + ); } catch (e) { - emit(state.copyWith( - isConfirmingPayment: false, - isPaymentConfirmed: false, - confirmationError: e.toString(), - )); + emit( + state.copyWith( + isConfirmingPayment: false, + isPaymentConfirmed: false, + confirmationError: e.toString(), + ), + ); } } -} \ No newline at end of file +} diff --git a/lib/postcard/models/my_postcard_model.dart b/lib/postcard/models/my_postcard_model.dart index 1ff827e..936d72d 100644 --- a/lib/postcard/models/my_postcard_model.dart +++ b/lib/postcard/models/my_postcard_model.dart @@ -170,4 +170,81 @@ class MyPostCard { 'updatedAt': updatedAt.toIso8601String(), }; } + + MyPostCard copyWith({ + int? id, + int? userXid, + String? pcTitle, + String? pcNumber, + String? cityName, + DateTime? pcDatetime, + String? pcContent, + String? pcImagePath, + bool? isForSelf, + String? fullname, + String? emailAddress, + String? isdCode, + String? mobileNumber, + String? address1, + String? address2, + String? zipCode, + String? stateName, + String? countryName, + String? orderStatus, + double? baseAmount, + int? couponXid, + double? couponDiscountPercent, + double? couponDiscountAmount, + double? totalTaxAmount, + double? totalAmount, + bool? isPaid, + String? paymentMode, + String? paymentId, + String? paymentStatus, + String? paymentIntentId, + bool? isDraft, + DateTime? deliveredOn, + bool? isActive, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return MyPostCard( + id: id ?? this.id, + userXid: userXid ?? this.userXid, + pcTitle: pcTitle ?? this.pcTitle, + pcNumber: pcNumber ?? this.pcNumber, + cityName: cityName ?? this.cityName, + pcDatetime: pcDatetime ?? this.pcDatetime, + pcContent: pcContent ?? this.pcContent, + pcImagePath: pcImagePath ?? this.pcImagePath, + isForSelf: isForSelf ?? this.isForSelf, + fullname: fullname ?? this.fullname, + emailAddress: emailAddress ?? this.emailAddress, + isdCode: isdCode ?? this.isdCode, + mobileNumber: mobileNumber ?? this.mobileNumber, + address1: address1 ?? this.address1, + address2: address2 ?? this.address2, + zipCode: zipCode ?? this.zipCode, + stateName: stateName ?? this.stateName, + countryName: countryName ?? this.countryName, + orderStatus: orderStatus ?? this.orderStatus, + baseAmount: baseAmount ?? this.baseAmount, + couponXid: couponXid ?? this.couponXid, + couponDiscountPercent: + couponDiscountPercent ?? this.couponDiscountPercent, + couponDiscountAmount: couponDiscountAmount ?? this.couponDiscountAmount, + totalTaxAmount: totalTaxAmount ?? this.totalTaxAmount, + totalAmount: totalAmount ?? this.totalAmount, + isPaid: isPaid ?? this.isPaid, + paymentMode: paymentMode ?? this.paymentMode, + paymentId: paymentId ?? this.paymentId, + paymentStatus: paymentStatus ?? this.paymentStatus, + paymentIntentId: paymentIntentId ?? this.paymentIntentId, + isDraft: isDraft ?? this.isDraft, + deliveredOn: deliveredOn ?? this.deliveredOn, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } } diff --git a/lib/postcard/repository/my_postcard_repository.dart b/lib/postcard/repository/my_postcard_repository.dart index 2a5932b..61e63f7 100644 --- a/lib/postcard/repository/my_postcard_repository.dart +++ b/lib/postcard/repository/my_postcard_repository.dart @@ -1,3 +1,7 @@ +import 'dart:developer'; + +import 'package:dio/dio.dart'; + import '../../networkApiServices/network_api_services.dart'; import '../../networkApiServices/api_urls.dart'; import '../models/my_postcard_model.dart'; @@ -13,8 +17,61 @@ class MyPostCardsRepository { url: '${ApiUrls.myPostCards}?type=$type', ); - return (response.data as List) - .map((e) => MyPostCard.fromJson(e)) - .toList(); + return (response.data as List).map((e) => MyPostCard.fromJson(e)).toList(); + } + + Future editMyPostCards({required MyPostCard postcard}) async { + try { + final formData = FormData(); + + formData.fields.addAll([ + MapEntry('countryName', postcard.countryName), + MapEntry('cityName', postcard.cityName), + MapEntry('stateName', postcard.stateName), + MapEntry('zipCode', postcard.zipCode), + MapEntry('pcTitle', postcard.pcTitle), + MapEntry('pcContent', postcard.pcContent), + MapEntry('pcNumber', postcard.pcNumber), + MapEntry('pcDatetime', postcard.pcDatetime.toString()), + MapEntry('fullname', postcard.fullname), + MapEntry('isdCode', postcard.isdCode), + ]); + + if (postcard.address1.isNotEmpty) { + formData.fields.add(MapEntry('address1', postcard.address1)); + } + + if (postcard.address2.isNotEmpty) { + formData.fields.add(MapEntry('address2', postcard.address2)); + } + // final fileName = postcard.pcImagePath.split('/').last; + // formData.files.add( + // MapEntry( + // 'pcImage', + // await MultipartFile.fromFile( + // postcard.pcImagePath, + // filename: fileName, + // ), + // ), + // ); + await _apiService.putApi( + url: '${ApiUrls.editPostcard}/${postcard.id}', + data: formData, + ); + return; + } catch (e, stack) { + log("Edit PostCard Error - $e"); + log("Edit PostCard Error - $stack"); + } + } + + Future deleteMyPostCards(int id) async { + try { + await _apiService.deleteApi(url: '${ApiUrls.editPostcard}/$id'); + return; + } catch (e, stack) { + log("Delete PostCard Error - $e"); + log("Delete PostCard Error - $stack"); + } } } diff --git a/lib/postcard/views/edit_postcard_view.dart b/lib/postcard/views/edit_postcard_view.dart new file mode 100644 index 0000000..db2c04d --- /dev/null +++ b/lib/postcard/views/edit_postcard_view.dart @@ -0,0 +1,376 @@ +import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart'; +import 'package:citycards_customer/postcard/models/my_postcard_model.dart'; +import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.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/custom_text.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../widgets/edit_post_card/edit_message.dart'; +import '../widgets/edit_post_card/your_details.dart'; + +class EditPostcardView extends StatefulWidget { + final MyPostCard myPostCard; + const EditPostcardView({super.key, required this.myPostCard}); + + @override + State createState() => _EditPostcardViewState(); +} + +class _EditPostcardViewState extends State { + MyPostCard? postCard; + final EditPostcardBloc editPostcardBloc = EditPostcardBloc(); + + final _formKey = GlobalKey(); + + final _fullNameController = TextEditingController(); + final _addressController = TextEditingController(); + final _cityController = TextEditingController(); + final _zipCodeController = TextEditingController(); + + String? _selectedCountry; + String? _selectedState; + + @override + void dispose() { + _fullNameController.dispose(); + _addressController.dispose(); + _cityController.dispose(); + _zipCodeController.dispose(); + super.dispose(); + } + + @override + void initState() { + setState(() { + postCard = widget.myPostCard; + _fullNameController.text = widget.myPostCard.fullname; + _addressController.text = widget.myPostCard.address1; + _cityController.text = widget.myPostCard.cityName; + _zipCodeController.text = widget.myPostCard.zipCode; + _selectedCountry = widget.myPostCard.countryName; + _selectedState = widget.myPostCard.stateName; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + return Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: Colors.white, + body: BlocConsumer( + bloc: editPostcardBloc, + listener: (ctxx, state) async { + if (state is EditPostcardSuccessfull) { + if (Navigator.canPop(ctxx)) { + Navigator.pop(ctxx, true); + } + } else if (state is EditPostcardError) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(state.error))); + } + }, + builder: (context, state) { + return Stack( + children: [ + SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showCart: true, + showDivider: true, + ), + Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon(Icons.arrow_back), + ), + SizedBox(width: 8.w), + CustomText(text: "Edit Postcard", size: 12.sp), + ], + ), + SizedBox(height: 10.h), + Text( + "Upload Image", + style: GoogleFonts.poppins( + color: Color(0XFF212121), + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 2.h), + Text( + "Edit your own unique postcards by uploading images that capture your unforgettable moments.", + style: GoogleFonts.poppins( + color: Color(0XFF000000).withValues(alpha: 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 10.h), + Row( + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: CustomPaint( + painter: DottedBorderPainter(), + child: Container( + padding: EdgeInsets.all(10), + height: size.width * 0.45, + width: size.width, + constraints: BoxConstraints(minHeight: 150), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + '${ApiUrls.baseUrl}${postCard!.pcImagePath}', + height: size.width * 0.45, + width: size.width, + fit: BoxFit.cover, + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) + return child; + return Container( + height: size.width * 0.45, + width: size.width, + color: Colors.grey[300], + child: const Center( + child: + CircularProgressIndicator( + color: Color(0xffF95F62), + strokeWidth: 2, + ), + ), + ); + }, + errorBuilder: + (context, error, stackTrace) { + return Container( + height: size.width * 0.45, + width: size.width, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, + ), + ), + ), + ), + ), + Expanded( + child: Container( + height: size.width * 0.5, + width: size.width, + constraints: BoxConstraints(minHeight: 150), + padding: EdgeInsets.all(10), + child: Column( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + imageButton( + title: 'Take a photo', + icon: Icons.camera_alt_outlined, + width: size.width, + ), + imageButton( + title: 'Upload Again', + icon: Icons.refresh, + width: size.width, + ), + imageButton( + title: 'Edit Filters', + width: size.width, + ), + ], + ), + ), + ), + ], + ), + SizedBox(height: 10.h), + Text( + "Edit message", + style: GoogleFonts.poppins( + color: Color(0XFF212121), + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 2.h), + Text( + "Edit your own unique postcards to cherish your unforgettable moments.", + style: GoogleFonts.poppins( + color: Color(0XFF000000).withValues(alpha: 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 10.h), + EditMessage( + text: postCard!.pcContent, + onChange: (message, font) { + postCard = postCard!.copyWith( + pcContent: getFormattedMessage(message, font), + ); + }, + ), + SizedBox(height: 10.h), + Form( + key: _formKey, + child: EditYourdetails( + fullNameController: _fullNameController, + addressController: _addressController, + cityController: _cityController, + zipCodeController: _zipCodeController, + selectedCountry: _selectedCountry ?? "", + selectedState: _selectedState ?? "", + formKey: _formKey, + selectState: (String p1) { + setState(() { + _selectedState = p1; + }); + }, + selectCountry: (String p1) { + setState(() { + _selectedCountry = p1; + }); + }, + ), + ), + + const SizedBox(height: 30), + + // Next Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + postCard = postCard!.copyWith( + fullname: _fullNameController.text, + address1: _addressController.text, + cityName: _cityController.text, + zipCode: _zipCodeController.text, + stateName: _selectedState, + countryName: _selectedCountry, + ); + editPostcardBloc.add( + EditPostCard(myPostCard: postCard!), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ), + + Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: state is EditPostcardSuccessfull + ? Center( + child: SizedBox( + width: 25, + height: 25, + child: CircularProgressIndicator( + color: Color(0XFFF95F62), + ), + ), + ) + : SizedBox(), + ), + ], + ); + }, + ), + ); + } + + Widget imageButton({ + Function()? onPressed, + required String title, + IconData? icon, + required double width, + }) { + return SizedBox( + width: width, + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + side: const BorderSide(color: Color(0xffF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + style: TextStyle( + color: Color(0xffF95F62), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: icon != null ? 8 : 0), + icon != null ? Icon(icon, color: Color(0xffF95F62)) : SizedBox(), + ], + ), + ), + ); + } + + String getFormattedMessage(String message, String selectedFont) { + if (message.isEmpty) { + return ''; + } + + if (selectedFont.isEmpty) { + // Default font (Poppins) + return '$message'; + } + + return '$message'; + } +} diff --git a/lib/postcard/views/my_postcard_drafts_view.dart b/lib/postcard/views/my_postcard_drafts_view.dart index a9a5502..6553949 100644 --- a/lib/postcard/views/my_postcard_drafts_view.dart +++ b/lib/postcard/views/my_postcard_drafts_view.dart @@ -1,6 +1,11 @@ +import 'dart:developer'; + +import 'package:citycards_customer/postcard/blocs/edit_postcard/edit_postcard_bloc.dart'; +import 'package:citycards_customer/postcard/views/edit_postcard_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; import '../../core/route_constants.dart'; @@ -22,9 +27,7 @@ class MyPostCardDraftView extends StatelessWidget { // Show loading indicator if drafts are loading if (state.isDraftLoading && state.draftPostCards.isEmpty) { return const Center( - child: CircularProgressIndicator( - color: Color(0xffF95F62), - ), + child: CircularProgressIndicator(color: Color(0xffF95F62)), ); } @@ -79,19 +82,43 @@ class MyPostCardDraftView extends StatelessWidget { } // Show the list of drafts - return RefreshIndicator( - onRefresh: () async { - context.read().add(const RefreshDraftPostCards()); - }, - color: const Color(0xffF95F62), - child: ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: state.draftPostCards.length, - itemBuilder: (context, index) { - final postcard = state.draftPostCards[index]; - return _buildDraftCard(context, postcard); - }, - ), + return Stack( + children: [ + RefreshIndicator( + onRefresh: () async { + context.read().add( + const RefreshDraftPostCards(), + ); + }, + color: const Color(0xffF95F62), + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: state.draftPostCards.length, + itemBuilder: (context, index) { + final postcard = state.draftPostCards[index]; + return _buildDraftCard(context, postcard); + }, + ), + ), + + Positioned( + top: 0, + left: 0, + right: 0, + bottom: 0, + child: state.isDeleteLoading == true + ? Center( + child: SizedBox( + width: 25, + height: 25, + child: CircularProgressIndicator( + color: Color(0XFFF95F62), + ), + ), + ) + : SizedBox(), + ), + ], ); } @@ -130,11 +157,16 @@ class MyPostCardDraftView extends StatelessWidget { const SizedBox(height: 24), ElevatedButton( onPressed: () { - context.read().add(const FetchDraftPostCards()); + context.read().add( + const FetchDraftPostCards(), + ); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h), + padding: EdgeInsets.symmetric( + horizontal: 32.w, + vertical: 12.h, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40), ), @@ -160,131 +192,213 @@ class MyPostCardDraftView extends StatelessWidget { Widget _buildDraftCard(BuildContext context, MyPostCard postcard) { return Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: const Color(0xffF95F62).withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xffF1F5F7)), + color: const Color(0xffF95F62), + borderRadius: BorderRadius.circular(10), + border: Border(left: BorderSide(width: 6, color: Color(0XFFF93232))), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /// LEFT IMAGE - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.network( - '${ApiUrls.baseUrl}${postcard.pcImagePath}', - height: 72, - width: 72, - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Container( - height: 72, - width: 72, - color: Colors.grey[300], - child: const Center( - child: CircularProgressIndicator( - color: Color(0xffF95F62), - strokeWidth: 2, + margin: EdgeInsets.only(bottom: 15), + child: Slidable( + key: UniqueKey(), + startActionPane: ActionPane( + motion: const ScrollMotion(), + + // dismissible: DismissiblePane(onDismissed: () {}), + children: [ + SlidableAction( + onPressed: (ctx) { + context.read().add( + DeleteDraftPostCards(id: postcard.id), + ); + }, + flex: 3, + backgroundColor: Color(0XFFF93232), + foregroundColor: Colors.white, + icon: Icons.delete, + label: 'Delete', + autoClose: true, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + bottomLeft: Radius.circular(10), + ), + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration(color: const Color(0XFFFFF5F5)), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// NUMBER + Text( + "#${postcard.pcNumber}", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black.withValues(alpha: 0.4), + ), + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + /// LEFT IMAGE + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + '${ApiUrls.baseUrl}${postcard.pcImagePath}', + height: 72, + width: 72, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 72, + width: 72, + color: Colors.grey[300], + child: const Center( + child: CircularProgressIndicator( + color: Color(0xffF95F62), + strokeWidth: 2, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 72, + width: 72, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, ), ), - ); - }, - errorBuilder: (context, error, stackTrace) { - return Container( - height: 72, - width: 72, - color: Colors.grey[300], - child: const Icon( - Icons.image_not_supported, - color: Colors.grey, + + const SizedBox(width: 14), + + /// RIGHT CONTENT + Expanded( + child: Text( + postcard.pcTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + + style: GoogleFonts.poppins( + fontSize: 15, + fontWeight: FontWeight.w400, + color: Colors.black87, + ), + ), ), - ); - }, - ), + ], + ), + const SizedBox(height: 10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider( + create: (context) => EditPostcardBloc(), + child: EditPostcardView(myPostCard: postcard), + ), + ), + ); + + if (result == true) { + // ignore: use_build_context_synchronously + context.read().add( + const RefreshDraftPostCards(), + ); + } + }, + + style: ElevatedButton.styleFrom( + backgroundColor: Color( + 0xfff95f62, + ).withValues(alpha: 0.1), + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + side: BorderSide(color: Color(0XFFFDCDCE)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.edit_outlined, + size: 22, + color: Color(0XFFF95F62), + ), + SizedBox(width: 5), + Text( + "Edit", + style: GoogleFonts.poppins( + color: Color(0XFFF95F62), + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Color( + 0xfff95f62, + ).withValues(alpha: 0.1), + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + side: BorderSide(color: Color(0XFFFDCDCE)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Transform.rotate( + angle: -45, + child: Icon( + Icons.send_outlined, + size: 22, + color: Color(0XFFF95F62), + ), + ), + SizedBox(width: 5), + Text( + "Send", + style: GoogleFonts.poppins( + color: Color(0XFFF95F62), + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + ], ), - - const SizedBox(width: 14), - - /// RIGHT CONTENT - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /// NUMBER - Text( - "#${postcard.pcNumber}", - style: GoogleFonts.poppins( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - - const SizedBox(height: 4), - - /// TITLE - Text( - postcard.pcTitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: GoogleFonts.poppins( - fontSize: 15, - fontWeight: FontWeight.w400, - color: Colors.black87, - ), - ), - - const SizedBox(height: 10), - - /// ICONS – BOTTOM RIGHT (UNDER TITLE) - Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () { - // delete - }, - child: Image.asset( - 'assets/icons/delete_icon.png', - width: 20, - height: 20, - ), - ), - const SizedBox(width: 16), - GestureDetector( - onTap: () { - // edit - }, - child: Image.asset( - 'assets/icons/edit_icon.png', - width: 20, - height: 20, - ), - ), - const SizedBox(width: 16), - GestureDetector( - onTap: () { - // send - }, - child: Image.asset( - 'assets/icons/send_icon.png', - width: 20, - height: 20, - ), - ), - ], - ), - ), - ], - ), - ), - ], + ), ), ); } -} \ No newline at end of file +} diff --git a/lib/postcard/views/my_postcard_orders_view.dart b/lib/postcard/views/my_postcard_orders_view.dart index 74811ad..ca0fc78 100644 --- a/lib/postcard/views/my_postcard_orders_view.dart +++ b/lib/postcard/views/my_postcard_orders_view.dart @@ -23,9 +23,7 @@ class MyPostCardOrdersView extends StatelessWidget { // Show loading indicator if orders are loading if (state.isOrderLoading && state.orderPostCards.isEmpty) { return const Center( - child: CircularProgressIndicator( - color: Color(0xffF95F62), - ), + child: CircularProgressIndicator(color: Color(0xffF95F62)), ); } @@ -131,11 +129,16 @@ class MyPostCardOrdersView extends StatelessWidget { const SizedBox(height: 24), ElevatedButton( onPressed: () { - context.read().add(const FetchOrderPostCards()); + context.read().add( + const FetchOrderPostCards(), + ); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h), + padding: EdgeInsets.symmetric( + horizontal: 32.w, + vertical: 12.h, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40), ), @@ -160,41 +163,71 @@ class MyPostCardOrdersView extends StatelessWidget { } Widget _buildOrderCard(BuildContext context, MyPostCard postcard) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Postcard Number above the card - Padding( - padding: const EdgeInsets.only(left: 4, bottom: 8), - child: Text( - "#${postcard.pcNumber}", - style: GoogleFonts.poppins( - color: Colors.black, - fontWeight: FontWeight.w500, - fontSize: 15.sp, - ), + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xffF95F62).withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xffF1F5F7)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + "#${postcard.pcNumber}", + style: GoogleFonts.poppins( + color: Colors.black.withValues(alpha: 0.4), + fontWeight: FontWeight.w400, + fontSize: 12.sp, + ), + ), + SizedBox(width: 10), + Spacer(), + Text( + "Status:", + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 10.sp, + ), + ), + SizedBox(width: 5), + Container( + padding: const EdgeInsets.fromLTRB(13, 7, 13, 7), + decoration: BoxDecoration( + color: _getStatusColor( + postcard.orderStatus, + ).withOpacity(0.16), + border: Border.all( + color: _getStatusBorderColor(postcard.orderStatus), + ), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + _getStatusText(postcard.orderStatus), + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 8.54.sp, + ), + ), + ), + ], ), - ), - - // Order Card - Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: const Color(0xffF95F62).withValues(alpha:0.08), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xffF1F5F7), - ), - ), - child: Row( + SizedBox(width: 10), + Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Postcard Image ClipRRect( borderRadius: BorderRadius.circular(8), child: Image( - image: NetworkImage('${ApiUrls.baseUrl}${postcard.pcImagePath}'), + image: NetworkImage( + '${ApiUrls.baseUrl}${postcard.pcImagePath}', + ), height: 70.h, width: 70.w, fit: BoxFit.cover, @@ -233,98 +266,67 @@ class MyPostCardOrdersView extends StatelessWidget { // Postcard Details Expanded( - child: SizedBox( + child: Container( + alignment: Alignment.centerLeft, height: 60.h, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - postcard.pcTitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: GoogleFonts.poppins( - color: Colors.black, - fontWeight: FontWeight.w400, - fontSize: 16.sp, - ), - ), - const SizedBox(height: 6), - Text( - "5 Post cards", - style: GoogleFonts.poppins( - color: Colors.black, - fontWeight: FontWeight.w400, - fontSize: 14.sp, - ), - ), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - padding: const EdgeInsets.fromLTRB(13, 7, 13, 7), - decoration: BoxDecoration( - color: _getStatusColor(postcard.orderStatus).withOpacity(0.16), - border: Border.all( - color: _getStatusBorderColor(postcard.orderStatus), - ), - borderRadius: BorderRadius.circular(16), - ), - child: Text( - _getStatusText(postcard.orderStatus), - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.w400, - fontSize: 8.54.sp, - ), - ), - ), - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MyPostcardPreviewView( - postcard: postcard, - ), - ), - ); - }, - child: Row( - children: [ - Icon( - Icons.remove_red_eye_outlined, - size: 15, - color: const Color(0xffF95F62), - ), - SizedBox(width: 5.w), - Text( - "Preview", - style: TextStyle( - fontWeight: FontWeight.w400, - color: const Color(0xffF95F62), - fontSize: 13.sp, - ), - ), - ], - ), - ), - ], - ), - ], + child: Text( + postcard.pcTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 16.sp, + ), ), ), ), ], ), - ), - ], + Container( + margin: EdgeInsets.only(top: 10), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xfff95f62).withValues(alpha: 0.1), + elevation: 0, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + side: BorderSide(color: Color(0XFFFDCDCE)), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MyPostcardPreviewView(postcard: postcard), + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.remove_red_eye_outlined, + size: 15, + color: const Color(0xffF95F62), + ), + SizedBox(width: 5.w), + Text( + "Preview", + style: TextStyle( + fontWeight: FontWeight.w400, + color: const Color(0xffF95F62), + fontSize: 13.sp, + ), + ), + ], + ), + ), + ), + ], + ), ); } @@ -382,4 +384,4 @@ class MyPostCardOrdersView extends StatelessWidget { return status; } } -} \ No newline at end of file +} diff --git a/lib/postcard/views/write_message_step_page_view.dart b/lib/postcard/views/write_message_step_page_view.dart index 47656aa..bba1b01 100644 --- a/lib/postcard/views/write_message_step_page_view.dart +++ b/lib/postcard/views/write_message_step_page_view.dart @@ -45,13 +45,30 @@ class _WriteMessageStepPageViewState extends State { final bloc = context.read(); _controller.text = state.message ?? ""; _controller.selection = TextSelection.fromPosition( - TextPosition(offset: _controller.text.length)); + TextPosition(offset: _controller.text.length), + ); final fonts = [ - {"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"}, - {"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"}, - {"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"}, - {"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"}, + { + "name": "Default", + "font": GoogleFonts.poppins(), + "cleanName": "Poppins", + }, + { + "name": "Patrick Hand", + "font": GoogleFonts.patrickHand(), + "cleanName": "Patrick Hand", + }, + { + "name": "Indie Flower", + "font": GoogleFonts.indieFlower(), + "cleanName": "Indie Flower", + }, + { + "name": "Gloria Hallelujah", + "font": GoogleFonts.gloriaHallelujah(), + "cleanName": "Gloria Hallelujah", + }, ]; return SafeArea( @@ -60,12 +77,17 @@ class _WriteMessageStepPageViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), StepProgressBar(totalSteps: 4, currentStep: 3), const SizedBox(height: 24), - Text("Write a message", - style: - TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), + Text( + "Write a message", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600), + ), const SizedBox(height: 6), Text( "Design your own unique postcards to cherish your unforgettable moments.", @@ -133,7 +155,8 @@ class _WriteMessageStepPageViewState extends State { final String fontName = font["name"] as String; final String cleanName = font["cleanName"] as String; - final isSelected = state.selectedFont == cleanName || + final isSelected = + state.selectedFont == cleanName || (state.selectedFont == null && fontName == "Default"); return GestureDetector( @@ -160,20 +183,24 @@ class _WriteMessageStepPageViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("Aa", - style: fontStyle.copyWith( - fontSize: 24.sp, - color: const Color(0xff1A1A1A), - )), + Text( + "Aa", + style: fontStyle.copyWith( + fontSize: 24.sp, + color: const Color(0xff1A1A1A), + ), + ), const SizedBox(height: 4), - Text(fontName, - textAlign: TextAlign.center, - style: fontStyle.copyWith( - fontSize: 11.sp, - color: isSelected - ? const Color(0xffF95F62) - : const Color(0xff2D3134), - )), + Text( + fontName, + textAlign: TextAlign.center, + style: fontStyle.copyWith( + fontSize: 11.sp, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + ), + ), ], ), ), @@ -218,7 +245,10 @@ class _WriteMessageStepPageViewState extends State { } // Helper method to get the correct font style for the text field - TextStyle _getTextFieldStyle(String? selectedFont, List> fonts) { + TextStyle _getTextFieldStyle( + String? selectedFont, + List> fonts, + ) { if (selectedFont == null || selectedFont.isEmpty) { return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); } @@ -260,10 +290,12 @@ class DottedBorderPainter extends CustomPainter { ..style = PaintingStyle.stroke; final path = Path() - ..addRRect(RRect.fromRectAndRadius( - Rect.fromLTWH(0, 0, size.width, size.height), - Radius.circular(borderRadius), - )); + ..addRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + Radius.circular(borderRadius), + ), + ); // Create dashed path final dashPath = _createDashedPath(path, dashWidth, dashSpace); @@ -319,14 +351,10 @@ class LinedPaperPainter extends CustomPainter { const lineSpacing = 30.0; for (double i = lineSpacing; i < size.height; i += lineSpacing) { - canvas.drawLine( - Offset(0, i), - Offset(size.width, i), - paint, - ); + canvas.drawLine(Offset(0, i), Offset(size.width, i), paint); } } @override bool shouldRepaint(LinedPaperPainter oldDelegate) => false; -} \ No newline at end of file +} diff --git a/lib/postcard/widgets/edit_post_card/edit_message.dart b/lib/postcard/widgets/edit_post_card/edit_message.dart new file mode 100644 index 0000000..3e7b81d --- /dev/null +++ b/lib/postcard/widgets/edit_post_card/edit_message.dart @@ -0,0 +1,257 @@ +import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:html/parser.dart' as html_parser; + +class EditMessage extends StatefulWidget { + final String text; + final Function(String, String) onChange; + const EditMessage({super.key, required this.text, required this.onChange}); + + @override + State createState() => _EditMessageState(); +} + +class _EditMessageState extends State { + final TextEditingController _controller = TextEditingController(); + final fonts = [ + {"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"}, + { + "name": "Patrick Hand", + "font": GoogleFonts.patrickHand(), + "cleanName": "Patrick Hand", + }, + { + "name": "Indie Flower", + "font": GoogleFonts.indieFlower(), + "cleanName": "Indie Flower", + }, + { + "name": "Gloria Hallelujah", + "font": GoogleFonts.gloriaHallelujah(), + "cleanName": "Gloria Hallelujah", + }, + ]; + + String selectedFont = "Poppins"; + + @override + void initState() { + final parsedMessage = _parseHtmlMessage(widget.text); + final messageText = parsedMessage['text'] ?? ''; + final fontFamily = parsedMessage['fontFamily'] ?? ''; + setState(() { + _controller.text = messageText; + selectedFont = fontFamily; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(12), + ), + child: CustomPaint( + painter: LinedPaperPainter(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextField( + controller: _controller, + maxLines: 8, + maxLength: 400, + cursorColor: const Color(0xffF95F62), + style: _getTextFieldStyle(selectedFont, fonts), + decoration: InputDecoration( + border: InputBorder.none, + hintText: "Add Your Message Here", + hintStyle: TextStyle( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + counterText: "", + ), + onChanged: (val) { + widget.onChange(val, selectedFont); + }, + ), + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(top: 6, right: 8), + child: Text( + "${_controller.text.length}/400", + style: TextStyle(fontSize: 12.sp, color: const Color(0xff999999)), + ), + ), + ), + + const SizedBox(height: 20), + + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: fonts.map((font) { + final TextStyle fontStyle = font['font'] as TextStyle; + final String fontName = font["name"] as String; + final String cleanName = font["cleanName"] as String; + + final isSelected = selectedFont == cleanName; + + return GestureDetector( + onTap: () { + setState(() { + selectedFont = cleanName; + }); + widget.onChange(_controller.text, selectedFont); + }, + child: Container( + padding: const EdgeInsets.all(6), + width: 100.w, + height: 100.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: CustomPaint( + painter: DottedBorderPainter( + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + strokeWidth: 1.5, + dashWidth: 4, + dashSpace: 3, + borderRadius: 12, + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Aa", + style: fontStyle.copyWith( + fontSize: 24.sp, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 4), + Text( + fontName, + textAlign: TextAlign.center, + style: fontStyle.copyWith( + fontSize: 11.sp, + color: isSelected + ? const Color(0xffF95F62) + : const Color(0xff2D3134), + ), + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ], + ); + } + + TextStyle _getTextFieldStyle( + String? selectedFont, + List> fonts, + ) { + if (selectedFont == null || selectedFont.isEmpty) { + return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); + } + + // Find matching font by cleanName + for (var font in fonts) { + if (font['cleanName'] == selectedFont) { + final TextStyle fontStyle = font['font'] as TextStyle; + return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black); + } + } + + // Default fallback to Poppins + return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black); + } + + Map _parseHtmlMessage(String htmlMessage) { + if (htmlMessage.isEmpty) { + return {'text': '', 'fontFamily': ''}; + } + + // Check if message contains HTML tags + if (!htmlMessage.contains(' formKey; + final Function(String) selectState; + final Function(String) selectCountry; + const EditYourdetails({ + super.key, + required this.fullNameController, + required this.addressController, + required this.cityController, + required this.zipCodeController, + required this.selectedCountry, + required this.selectedState, + required this.formKey, + required this.selectState, + required this.selectCountry, + }); + + @override + State createState() => _EditYourdetailsState(); +} + +class _EditYourdetailsState extends State { + String? _selectedState; + String? _selectedCountry; + + @override + void initState() { + setState(() { + _selectedState = widget.selectedState; + _selectedCountry = widget.selectedCountry; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Recipient Details", + style: GoogleFonts.poppins( + color: Color(0XFF212121), + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 2.h), + Text( + "Enter the address of the person who will receive this postcard", + style: GoogleFonts.poppins( + color: Color(0XFF000000).withValues(alpha: 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 16), + + _buildInputField( + label: "Recipient", + hint: "Enter the recipient's name", + controller: widget.fullNameController, + ), + _buildInputField( + label: "Address", + hint: "Enter the recipient's Address", + controller: widget.addressController, + ), + _buildInputField( + label: "City", + hint: "Enter the name of your city", + controller: widget.cityController, + ), + _buildDropdownField( + label: "State", + hint: "Select your state", + value: _selectedState, + onChanged: (val) { + setState(() { + _selectedState = val; + }); + widget.selectState; + }, + ), + _buildInputField( + label: "Zip Code", + hint: "Enter the Zip Code you reside in", + controller: widget.zipCodeController, + keyboardType: TextInputType.number, + ), + _buildDropdownField( + label: "Country", + hint: "Select your country", + value: _selectedCountry, + onChanged: (val) { + setState(() { + _selectedCountry = val; + }); + widget.selectCountry; + }, + ), + ], + ); + } + + Widget _buildInputField({ + required String label, + required String hint, + required TextEditingController controller, + IconData? icon, + TextInputType? keyboardType, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 6), + TextFormField( + controller: controller, + keyboardType: keyboardType, + decoration: InputDecoration( + hintText: hint, + hintStyle: GoogleFonts.poppins( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + suffixIcon: icon != null + ? Icon(icon, color: Colors.black, size: 20) + : null, + contentPadding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 12, + ), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + errorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter $label'; + } + if (label == "Email ID" && !value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, + ), + ], + ), + ); + } + + /// 🔹 Dropdown input + Widget _buildDropdownField({ + required String label, + required String hint, + required String? value, + required Function(String?) onChanged, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 6), + DropdownButtonFormField( + value: value, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 12, + ), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + errorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xffFDCDCE), + ), + hint: Text( + hint, + style: GoogleFonts.poppins( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + ), + items: const [ + DropdownMenuItem( + value: "Lorem Ipsum", + child: Text("Lorem Ipsum"), + ), + // Add more items as needed + ], + onChanged: onChanged, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select $label'; + } + return null; + }, + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c3d6afb..f946ea1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -363,6 +363,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.9.3" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: ea369262929d3cc6ebf9d8a00c196127966f117fe433a5e5cb47fb08008ca203 + url: "https://pub.dev" + source: hosted + version: "4.0.3" flutter_stripe: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b87fc0e..615ca89 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: geocoding: ^4.0.0 cached_network_image: ^3.4.1 bloc: ^9.2.0 + flutter_slidable: ^4.0.3 dev_dependencies: flutter_test: From b08e2699e9bb27e804ee6bdd3af64b62ee9c345e Mon Sep 17 00:00:00 2001 From: mystery012728 Date: Fri, 13 Feb 2026 15:27:14 +0530 Subject: [PATCH 7/8] added my passes and more chnages --- assets/icons/calendar.png | Bin 0 -> 863 bytes assets/icons/person.png | Bin 0 -> 1707 bytes assets/icons/time.png | Bin 0 -> 3595 bytes .../bloc/stripe_payment_bloc.dart | 42 +- lib/StripePayment/view/stripe_payment.dart | 21 +- lib/add_details/add_details_view.dart | 14 +- .../widgets/share_bottomsheet.dart | 35 +- lib/attractions/models/attraction_model.dart | 39 +- lib/attractions/widget/attraction_card.dart | 29 +- lib/buy_a_pass/models/buy_pass_model.dart | 248 ++++---- lib/buy_a_pass/widget/payment_card_view.dart | 2 +- .../blocs/myPassCart/my_pass_cart_bloc.dart | 110 +++- .../blocs/myPassCart/my_pass_cart_event.dart | 8 + .../blocs/myPassCart/my_pass_cart_state.dart | 14 +- lib/cart/blocs/pass_bloc.dart | 80 +-- lib/cart/model/my_passes_cart_mode.dart | 207 +++++++ .../repository/my_pass_cart_repository.dart | 50 +- lib/cart/views/my_pass_cart_page_view.dart | 572 +++++++++++++++--- lib/checkout/bloc/checkOut/checkout_bloc.dart | 11 + .../bloc/checkOut/checkout_state.dart | 4 + .../pass_purchase_details_repository.dart | 8 +- lib/checkout/view/checkout_view.dart | 76 ++- .../pass_purchase_details_bottomsheet.dart | 12 +- lib/common_packages/common_app_texts.dart | 2 +- lib/core/app_router.dart | 28 +- lib/core/inside_bottom_navigator.dart | 38 +- .../view/create_account_view.dart | 173 +++++- lib/home/model/city_selection_model.dart | 46 +- lib/home/widgets/search_city_bottomsheet.dart | 6 +- .../bloc/get_itinerary_bloc.dart | 175 +++++- .../views/magic_itinerary_view.dart | 4 +- lib/localPreference/local_database.dart | 11 +- lib/login/view/verify_otp_bottomsheet.dart | 3 + lib/main.dart | 10 + .../blocs/myPasses/my_passes_bloc.dart | 85 +++ .../blocs/myPasses/my_passes_event.dart | 50 ++ .../blocs/myPasses/my_passes_state.dart | 39 ++ .../my_passes_attractions_bloc.dart | 72 +++ .../my_passes_attractions_event.dart | 30 + .../my_passes_attractions_state.dart | 64 ++ .../my_passes_details_bloc.dart | 30 + .../my_passes_details_event.dart | 17 + .../my_passes_details_state.dart | 32 + .../myPassesOffers/my_passes_offers_bloc.dart | 67 ++ .../my_passes_offers_event.dart | 16 + .../my_passes_offers_state.dart | 22 + .../models/my_passes_details_model.dart | 167 +++++ lib/my_pass/models/my_passes_model.dart | 119 ++++ .../my_passes_attractions_repository.dart | 29 + .../my_passes_details_repository.dart | 18 + .../my_passes_offers_repository.dart | 25 + .../repository/my_passes_repository.dart | 32 + lib/my_pass/views/my_pass_page_view.dart | 534 +++++++++++----- .../views/pass_attraction_details_view.dart | 135 ++++- .../views/pass_attractions_page_view.dart | 121 ++-- lib/my_pass/views/pass_details_page_view.dart | 529 +++++++++++----- .../search_pass_offers_with_listing.dart | 209 ++++--- lib/my_pass/widgets/pass_attraction_card.dart | 182 +++--- lib/my_pass/widgets/pass_widget.dart | 72 +-- lib/networkApiServices/api_urls.dart | 9 +- .../offer_pass_detail_view.dart | 23 +- .../add_to_cart_postcard_bloc.dart | 63 ++ .../add_to_cart_postcard_event.dart | 64 ++ .../add_to_cart_postcard_state.dart | 48 ++ .../postcard_checkout_bloc.dart | 70 +-- .../postcard_checkout_event.dart | 6 +- .../blocs/postcard_creation_bloc.dart | 13 + .../blocs/postcard_creation_events.dart | 23 + .../blocs/postcard_creation_state.dart | 45 +- .../postcard_add_to_cart_repository.dart | 203 +++++++ .../postcard_checkout_repository.dart | 178 ++---- .../views/add_filter_step_page_view.dart | 22 +- .../views/my_postcard_preview_view.dart | 102 ++-- .../views/order_success_page_view.dart | 12 +- .../views/postcard_checkout_page_view.dart | 40 +- .../views/postcard_creation_page_view.dart | 67 +- .../postcard_purchase_form_page_view.dart | 464 +++++++++----- .../preview_postcard_step_page_view.dart | 24 +- .../views/write_message_step_page_view.dart | 22 +- lib/postcard/widgets/back_card_widget.dart | 22 +- .../purchase_details_bottom_sheet.dart | 15 + .../view/edit_profile/edit_profile_view.dart | 155 ++++- lib/search_offers/model/offers_model.dart | 16 +- pubspec.lock | 8 + pubspec.yaml | 1 + 85 files changed, 5036 insertions(+), 1453 deletions(-) create mode 100644 assets/icons/calendar.png create mode 100644 assets/icons/person.png create mode 100644 assets/icons/time.png create mode 100644 lib/cart/model/my_passes_cart_mode.dart create mode 100644 lib/my_pass/blocs/myPasses/my_passes_bloc.dart create mode 100644 lib/my_pass/blocs/myPasses/my_passes_event.dart create mode 100644 lib/my_pass/blocs/myPasses/my_passes_state.dart create mode 100644 lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart create mode 100644 lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_event.dart create mode 100644 lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_state.dart create mode 100644 lib/my_pass/blocs/myPassesDetails/my_passes_details_bloc.dart create mode 100644 lib/my_pass/blocs/myPassesDetails/my_passes_details_event.dart create mode 100644 lib/my_pass/blocs/myPassesDetails/my_passes_details_state.dart create mode 100644 lib/my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart create mode 100644 lib/my_pass/blocs/myPassesOffers/my_passes_offers_event.dart create mode 100644 lib/my_pass/blocs/myPassesOffers/my_passes_offers_state.dart create mode 100644 lib/my_pass/models/my_passes_details_model.dart create mode 100644 lib/my_pass/models/my_passes_model.dart create mode 100644 lib/my_pass/repository/my_passes_attractions_repository.dart create mode 100644 lib/my_pass/repository/my_passes_details_repository.dart create mode 100644 lib/my_pass/repository/my_passes_offers_repository.dart create mode 100644 lib/my_pass/repository/my_passes_repository.dart create mode 100644 lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart create mode 100644 lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart create mode 100644 lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_state.dart create mode 100644 lib/postcard/repository/postcard_add_to_cart_repository.dart diff --git a/assets/icons/calendar.png b/assets/icons/calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..c25aff616168f27420f88dfd7977bb52312132dc GIT binary patch literal 863 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9EO-XP4l)OOlRpde#$ zkh>GZx^prwfgF}}M_)$E)e-c?47?{3zx;TbZFut90vGA6IfJ6CtpQiLrj_e0z zm#|!N402$5$QR=nwV)fPkh8|Bv9^5NAonP?rwUrTD*4Zk1E)R)PJs#eb0;@IEmKZYHOuHHIs;Q*?Ozy%v zbC(4ON9tQ_sTEK9us!(F`Kd`#r8VL6zmzU8Z=RIm+{^U-rd#N|w(9{)Ys+~yPM$LP zOr8bX*Ht+?G~ZZ0o2S;rAWe{%~Y+^$53;L1e3rUo~k(ZBWL~AHvi$sO6e@w ze6{XV&lh2)3ylsn8_V9st+PnDUa9`^##{D&$K?}>&wMlW;9jA?n9p--`VUh%EwM`0 zom>9OYh-Wf<^&3hsyyg*6w&bgpP`UYzx|B%3hTKsj@*UYeYmb0zD;7%@^D~L7$brhkjsbp$;BS`SYZqF~3 zzt|VHzTn-=3^V{?s(=oEluO_f?fqM>9h!Mf*V(|a?aaRAUv=kX-Zjb)7lx^2;l321 zs4;I<-paSHGyd_)o|Om2Crom~f|TtAlCE3za*O%iSf@2Re0I`&xWsqTzt0=1*!J3F zZ`!O>Q!tI~r~ZY|1N-;QJkr~;=X1mXi4DuLFBw+sn(6eh>%?4}YijS#Z&&ziJ=@~0drDELIAGL9O(c600d`2O+f$vv5yP2s;Gm@!@)MdWb*+ zO>l*Z`n53Z!ICIlAQ9%fR)-ql2%ZF)N2CbC?@D*ygkc0FQmR0s{ZEI4)B$^8riqZS zQ$72u3nfvSK%>^dp9B-D$qj$#<7)TxGn7Cn0*1J3z95RW4wbExBv$@5oilQ%-y&rZ~_}Fk`~B4N%Ctt8`Xl~8#us;K$En9;3Va= z_rTeC7LnxIhZ&Utjp(cd>&(q>DSZqnRwRMsh}7RUUQL#yTuZ6BmL#w`1V=b6x!(Ey z5t5o$_3LpVz6WZ2^%zJd!Zj;|E!nqb-OOzvzZwM2EXa9H`fk5{jEINe0Pc{SY=yz= ztSCO`cE24e#N00aIb8bS9$DanV@gRi-kb$K$3`bm5MnuHh$EDii*20+M{yzETotr~ zhJ^6h^|E?S>8c-Ok(WH7xjW1maR=7Y<6J+GO>NFT|Hi1@{ zKw>RiuaTU~NdwOonHw6T{#uk!xdoaGV`@AuI@iieQ9|WPHDup%t&=4`}l zDFj`~qlywGjY;yWv17B$MWkvs*o6#a66j(7;EP7+%(_NuH8r44*~z7?P+6CowLoIl zn_N6kzk;F%HsN1OjB-s_%rvr&de_#nt-TnFbDsVSsVj4Y%N9p?nrTd*kmY8;7qG<2 zf|4LGt#jF{bx%LQ3NmW7FVdWetrgi>kQQ%YLPmLYDX#=9f_N=R>CM@W>{$uK8=rBc z4i$9De)}d1vpT%CBYU=ckY_qLPMP9ZDO6h56+N)I9qcehuB&W4kK`TMTaVz);b#$O zXnE`fwgNeuIV=P2>b@9m;E3O9;31bl6-T4DMI&9^xb;oLxCsegZM(A@V24D=-t<8o z4`nVrdbUACf(2d)wXz^q|HgQ80YjOqj-5d1LzxYE9~FLJth4$1R{MHJa8>&2+GSxa z5PK}C@y@fBh#%PJVtKdBfrCkvllMq&O4f@m&}Ax^*;NytKb!`<45N4tQ`e}*H<~>n zd%xj0@r?7L@u^Tke&+ggg}btP8xh}1cXrCMqa;(0pa3!nB!8(vP ziliUk9o0lDz1P?_6l}sBl35`XFGz61#juFI;ouhuFV-`RYu3C*{sU>B)SOvhy}$qf002ovPDHLkV1j(9 BAb|h? literal 0 HcmV?d00001 diff --git a/assets/icons/time.png b/assets/icons/time.png new file mode 100644 index 0000000000000000000000000000000000000000..80428495b44014cd1df1faa6f35a168a4074ee7f GIT binary patch literal 3595 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 { final StripeService _stripeService; + // 🔒 Flag to prevent re-initialization after success + bool _paymentCompleted = false; + StripePaymentBloc({ StripeService? stripeService, }) : _stripeService = stripeService ?? StripeService(), @@ -24,6 +27,12 @@ class StripePaymentBloc extends Bloc { InitiatePayment event, Emitter emit, ) async { + // 🛑 Prevent re-initialization if payment already completed + if (_paymentCompleted) { + debugPrint('⚠️ Payment already completed. Ignoring re-initialization.'); + return; + } + try { emit(const StripePaymentLoading( message: 'Creating payment intent...', @@ -61,7 +70,8 @@ class StripePaymentBloc extends Bloc { // 3️⃣ Show Payment Sheet await Stripe.instance.presentPaymentSheet(); - // ✅ SUCCESS + // ✅ SUCCESS - Mark as completed + _paymentCompleted = true; emit(const StripePaymentSuccess()); } on StripeException catch (e) { _handleStripeException(e, emit); @@ -78,6 +88,12 @@ class StripePaymentBloc extends Bloc { InitiatePaymentWithClientSecret event, Emitter emit, ) async { + // 🛑 Prevent re-initialization if payment already completed + if (_paymentCompleted) { + debugPrint('⚠️ Payment already completed. Ignoring re-initialization.'); + return; + } + try { emit(const StripePaymentLoading( message: 'Initializing payment...', @@ -101,7 +117,8 @@ class StripePaymentBloc extends Bloc { // 2️⃣ Show Payment Sheet await Stripe.instance.presentPaymentSheet(); - // ✅ SUCCESS + // ✅ SUCCESS - Mark as completed + _paymentCompleted = true; emit(const StripePaymentSuccess()); } on StripeException catch (e) { _handleStripeException(e, emit); @@ -118,9 +135,12 @@ class StripePaymentBloc extends Bloc { CancelPaymentEvent event, Emitter emit, ) { - emit(const StripePaymentCancelled( - message: 'Payment cancelled by user', - )); + // Only emit cancelled if not already completed + if (!_paymentCompleted) { + emit(const StripePaymentCancelled( + message: 'Payment cancelled by user', + )); + } } /// Handle payment retry @@ -128,6 +148,9 @@ class StripePaymentBloc extends Bloc { RetryPaymentEvent event, Emitter emit, ) async { + // 🔄 Reset completion flag for retry + _paymentCompleted = false; + // Reset state first emit(const StripePaymentInitial()); @@ -142,6 +165,8 @@ class StripePaymentBloc extends Bloc { ResetPaymentState event, Emitter emit, ) { + // 🔄 Reset completion flag + _paymentCompleted = false; emit(const StripePaymentInitial()); } @@ -199,4 +224,11 @@ class StripePaymentBloc extends Bloc { return !nonRetryableErrors.contains(errorCode); } + + @override + Future close() { + // Reset flag on bloc disposal + _paymentCompleted = false; + return super.close(); + } } \ No newline at end of file diff --git a/lib/StripePayment/view/stripe_payment.dart b/lib/StripePayment/view/stripe_payment.dart index 25536e0..423f664 100644 --- a/lib/StripePayment/view/stripe_payment.dart +++ b/lib/StripePayment/view/stripe_payment.dart @@ -199,8 +199,18 @@ class StripePaymentScreen extends StatelessWidget { @override Widget build(BuildContext context) { return BlocConsumer( + // 🔒 CRITICAL: Only listen when state actually changes to prevent duplicate triggers + listenWhen: (previous, current) { + // Don't re-trigger if both states are the same success state + if (previous is StripePaymentSuccess && current is StripePaymentSuccess) { + debugPrint('⚠️ Preventing duplicate success listener'); + return false; + } + return true; + }, listener: (context, state) { if (state is StripePaymentSuccess) { + debugPrint('✅ Payment Success - Calling callback'); // ✅ Call the callback first onPaymentSuccess?.call(); // ✅ Then auto-close and return true after 1.5 seconds @@ -210,6 +220,7 @@ class StripePaymentScreen extends StatelessWidget { } }); } else if (state is StripePaymentFailure) { + debugPrint('❌ Payment Failure - ${state.error}'); onPaymentFailure?.call(state.error); // Auto-close after 2 seconds on failure Future.delayed(const Duration(seconds: 2), () { @@ -218,10 +229,18 @@ class StripePaymentScreen extends StatelessWidget { } }); } else if (state is StripePaymentCancelled) { + debugPrint('🚫 Payment Cancelled'); onPaymentCancelled?.call(); Navigator.of(context).pop(false); } }, + buildWhen: (previous, current) { + // 🔒 Prevent unnecessary rebuilds on duplicate success states + if (previous is StripePaymentSuccess && current is StripePaymentSuccess) { + return false; + } + return true; + }, builder: (context, state) { return Container( height: heightRatio == 1.0 @@ -394,7 +413,7 @@ class StripePaymentScreen extends StatelessWidget { onPressed: () { // Retry payment context.read().add( - InitiatePaymentWithClientSecret( + RetryPaymentEvent( clientSecret: clientSecret, ), ); diff --git a/lib/add_details/add_details_view.dart b/lib/add_details/add_details_view.dart index 0b79bf2..22c7626 100644 --- a/lib/add_details/add_details_view.dart +++ b/lib/add_details/add_details_view.dart @@ -81,12 +81,12 @@ class _AddDetailsViewState extends State { // Handle API submission success if (state is PurchaseDetailsSubmitted) { // Show success message - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Gift details submitted successfully!'), - backgroundColor: Color(0xffF95F62), - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Gift details submitted successfully!'), + // backgroundColor: Color(0xffF95F62), + // ), + // ); // Navigate back Navigator.of(context).pop('success'); @@ -231,7 +231,7 @@ class _AddDetailsViewState extends State { selectedCountry = value; }); }, - items: ["India", "USA", "UK", "Canada"] + items: ["Australia"] .map((value) { return DropdownMenuItem( value: value, diff --git a/lib/attraction_details/widgets/share_bottomsheet.dart b/lib/attraction_details/widgets/share_bottomsheet.dart index 8358ca0..98d0fc8 100644 --- a/lib/attraction_details/widgets/share_bottomsheet.dart +++ b/lib/attraction_details/widgets/share_bottomsheet.dart @@ -26,15 +26,18 @@ class ShareBottomSheet extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ + // drag handle Container( height: 4.h, width: 47.w, - margin: EdgeInsets.only(bottom: 16), + margin: EdgeInsets.only(bottom: 16.h), decoration: BoxDecoration( - color: Color(0xFF222222), + color: const Color(0xFF222222), borderRadius: BorderRadius.circular(8), ), ), + + // link field TextField( readOnly: true, decoration: InputDecoration( @@ -51,7 +54,10 @@ class ShareBottomSheet extends StatelessWidget { ), ), ), + SizedBox(height: 20.h), + + // grid GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -67,7 +73,16 @@ class ShareBottomSheet extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - Image.asset(item['icon']!, width: 55.w), + // FIXED SIZE ICON CONTAINER + Container( + width: 55.w, + height: 55.w, + alignment: Alignment.center, + child: Image.asset( + item['icon']!, + fit: BoxFit.contain, + ), + ), SizedBox(height: 8.h), Text( item['title']!, @@ -78,26 +93,32 @@ class ShareBottomSheet extends StatelessWidget { ); }, ), + const SizedBox(height: 20), + + // page indicator Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( 4, - (index) => Container( + (index) => Container( margin: const EdgeInsets.symmetric(horizontal: 3), width: 8.w, height: 8.h, decoration: BoxDecoration( - color: index == 0 ? Color(0xFF676363) : Colors.white, - border: Border.all(color: Color(0xFF676363)), + color: index == 0 + ? const Color(0xFF676363) + : Colors.white, + border: Border.all(color: const Color(0xFF676363)), shape: BoxShape.circle, ), ), ), ), + SizedBox(height: 10.h), ], ), ); } -} +} \ No newline at end of file diff --git a/lib/attractions/models/attraction_model.dart b/lib/attractions/models/attraction_model.dart index be46ea6..d2326c8 100644 --- a/lib/attractions/models/attraction_model.dart +++ b/lib/attractions/models/attraction_model.dart @@ -37,9 +37,9 @@ class Attraction { final String title; final String description; final String urlSlug; - final int cityXid; - final int cardTypeXid; - final int partnerXid; + final num cityXid; + final num cardTypeXid; + final num partnerXid; final String productCode; final bool isBookingRequired; @@ -47,14 +47,14 @@ class Attraction { final String bookingEmail; final String bookingPhoneNumber; - final double latitudeCoordinate; - final double longitudeCoordinate; + final num latitudeCoordinate; + final num longitudeCoordinate; final String address; - final double? ticketPriceAdult; - final double? ticketPriceChild; - final int durations; - final int groupSize; + final num? ticketPriceAdult; + final num? ticketPriceChild; + final num durations; + final num groupSize; final String ageRange; final String seoTitle; @@ -115,13 +115,11 @@ class Attraction { isPartnerAccess: json['isPartnerAccess'] ?? false, bookingEmail: json['bookingEmail'] ?? '', bookingPhoneNumber: json['bookingPhonenumber'] ?? '', - latitudeCoordinate: - (json['latitudeCoordinate'] as num?)?.toDouble() ?? 0.0, - longitudeCoordinate: - (json['longitudeCoordinate'] as num?)?.toDouble() ?? 0.0, + latitudeCoordinate: (json['latitudeCoordinate'] as num?) ?? 0, + longitudeCoordinate: (json['longitudeCoordinate'] as num?) ?? 0, address: json['address'] ?? '', - ticketPriceAdult: (json['ticketPriceAdult'] as num?)?.toDouble(), - ticketPriceChild: (json['ticketPriceChild'] as num?)?.toDouble(), + ticketPriceAdult: json['ticketPriceAdult'] as num?, + ticketPriceChild: json['ticketPriceChild'] as num?, durations: json['durations'] ?? 0, groupSize: json['groupSize'] ?? 0, ageRange: json['ageRange'] ?? '', @@ -197,9 +195,9 @@ class Attraction { class CardModel { final int id; final String title; - final int cardTypeXid; - final int adultPrice; - final int childPrice; + final num cardTypeXid; + final num adultPrice; + final num childPrice; final String cardStatus; CardModel({ @@ -234,7 +232,6 @@ class CardModel { } } - /* -------------------- GALLERY -------------------- */ class Gallery { @@ -275,7 +272,6 @@ class Gallery { bool get hasImage => filePathUrl.isNotEmpty; } - /* -------------------- CATEGORY -------------------- */ class Category { @@ -300,5 +296,4 @@ class Category { 'categoryName': categoryName, }; } -} - +} \ No newline at end of file diff --git a/lib/attractions/widget/attraction_card.dart b/lib/attractions/widget/attraction_card.dart index 170c985..fc0ec13 100644 --- a/lib/attractions/widget/attraction_card.dart +++ b/lib/attractions/widget/attraction_card.dart @@ -61,6 +61,8 @@ class AttractionCard extends StatelessWidget { children: [ Text( attraction.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, @@ -71,6 +73,8 @@ class AttractionCard extends StatelessWidget { Text( attraction.address, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: GoogleFonts.poppins( fontSize: 12.sp, fontWeight: FontWeight.w400, @@ -104,10 +108,8 @@ class AttractionCard extends StatelessWidget { ), SizedBox(height: 6.h), - /// TAGS (CARD TITLES) - attraction.isBookingRequired == false - ? Wrap( + Wrap( spacing: 6.w, runSpacing: 6.h, children: tags @@ -145,27 +147,6 @@ class AttractionCard extends StatelessWidget { ) .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, - ), - ), - ), ], ), ), diff --git a/lib/buy_a_pass/models/buy_pass_model.dart b/lib/buy_a_pass/models/buy_pass_model.dart index 73f5908..fb8858d 100644 --- a/lib/buy_a_pass/models/buy_pass_model.dart +++ b/lib/buy_a_pass/models/buy_pass_model.dart @@ -8,10 +8,10 @@ String buyPassModelToJson(BuyPassModel data) => json.encode(data.toJson()); class BuyPassModel { - final City city; - final List offers; - final List cards; - final List attractions; + City city; + List offers; + List cards; + List attractions; BuyPassModel({ required this.city, @@ -20,41 +20,49 @@ class BuyPassModel { required this.attractions, }); - factory BuyPassModel.fromJson(Map json) { + factory BuyPassModel.fromJson(Map? json) { + json ??= {}; + return BuyPassModel( city: City.fromJson(json['city']), - offers: List.from( - json['offers'].map((x) => Offer.fromJson(x)), - ), - cards: List.from( - json['cards'].map((x) => CardPass.fromJson(x)), - ), - attractions: List.from( - json['attractions'].map((x) => Attraction.fromJson(x)), - ), + offers: json['offers'] == null + ? [] + : List>.from(json['offers']) + .map((e) => Offer.fromJson(e)) + .toList(), + cards: json['cards'] == null + ? [] + : List>.from(json['cards']) + .map((e) => CardPass.fromJson(e)) + .toList(), + attractions: json['attractions'] == null + ? [] + : List>.from(json['attractions']) + .map((e) => Attraction.fromJson(e)) + .toList(), ); } Map toJson() => { "city": city.toJson(), - "offers": offers.map((x) => x.toJson()).toList(), - "cards": cards.map((x) => x.toJson()).toList(), - "attractions": attractions.map((x) => x.toJson()).toList(), + "offers": offers.map((e) => e.toJson()).toList(), + "cards": cards.map((e) => e.toJson()).toList(), + "attractions": attractions.map((e) => e.toJson()).toList(), }; } /// ---------- CITY ---------- class City { - final int id; - final String name; - final String slug; - final String tagLine; - final String description; - final String bestTimeToVisit; - final String priceRange; - final num individualTicketAmount; // Changed from int to num - final num cityCardTicketAmount; // Changed from int to num - final HeroBanner heroBanner; + int id; + String name; + String slug; + String tagLine; + String description; + String bestTimeToVisit; + String priceRange; + num individualTicketAmount; + num cityCardTicketAmount; + HeroBanner heroBanner; City({ required this.id, @@ -69,17 +77,19 @@ class City { required this.heroBanner, }); - factory City.fromJson(Map json) { + factory City.fromJson(Map? json) { + json ??= {}; + return City( - id: json['id'], - name: json['name'], - slug: json['slug'], - tagLine: json['tagLine'], - description: json['description'], - bestTimeToVisit: json['bestTimeToVisit'], - priceRange: json['priceRange'], - individualTicketAmount: json['individualTicketAmount'], - cityCardTicketAmount: json['cityCardTicketAmount'], + id: (json['id'] as num?)?.toInt() ?? 0, + name: json['name']?.toString() ?? "", + slug: json['slug']?.toString() ?? "", + tagLine: json['tagLine']?.toString() ?? "", + description: json['description']?.toString() ?? "", + bestTimeToVisit: json['bestTimeToVisit']?.toString() ?? "", + priceRange: json['priceRange']?.toString() ?? "", + individualTicketAmount: json['individualTicketAmount'] ?? 0, + cityCardTicketAmount: json['cityCardTicketAmount'] ?? 0, heroBanner: HeroBanner.fromJson(json['heroBanner']), ); } @@ -100,18 +110,20 @@ class City { /// ---------- HERO BANNER ---------- class HeroBanner { - final String title; - final String image; + String title; + String image; HeroBanner({ required this.title, required this.image, }); - factory HeroBanner.fromJson(Map json) { + factory HeroBanner.fromJson(Map? json) { + json ??= {}; + return HeroBanner( - title: json['title'], - image: json['image'], + title: json['title']?.toString() ?? "", + image: json['image']?.toString() ?? "", ); } @@ -123,25 +135,25 @@ class HeroBanner { /// ---------- OFFER ---------- class Offer { - final int id; - final String title; - final String offerCode; - final String? description; // ✅ optional - final String? redemptionLink; // ✅ optional - final String websiteBannerImage; - final String mobileBannerImage; - final String passType; - final DateTime startDateTime; - final DateTime endDateTime; - final String offerStatus; - final bool applyToPasses; + int id; + String title; + String offerCode; + String description; + String redemptionLink; + String websiteBannerImage; + String mobileBannerImage; + String passType; + DateTime startDateTime; + DateTime endDateTime; + String offerStatus; + bool applyToPasses; Offer({ required this.id, required this.title, required this.offerCode, - this.description, - this.redemptionLink, + required this.description, + required this.redemptionLink, required this.websiteBannerImage, required this.mobileBannerImage, required this.passType, @@ -151,20 +163,24 @@ class Offer { required this.applyToPasses, }); - factory Offer.fromJson(Map json) { + factory Offer.fromJson(Map? json) { + json ??= {}; + return Offer( - id: json['id'], - title: json['title'], - offerCode: json['offerCode'], - description: json['description'], // ✅ - redemptionLink: json['redemptionLink'], // ✅ - websiteBannerImage: json['websiteBannerImage'], - mobileBannerImage: json['mobileBannerImage'], - passType: json['passType'], - startDateTime: DateTime.parse(json['startDateTime']), - endDateTime: DateTime.parse(json['endDateTime']), - offerStatus: json['offerStatus'], - applyToPasses: json['applyToPasses'], + id: (json['id'] as num?)?.toInt() ?? 0, + title: json['title']?.toString() ?? "", + offerCode: json['offerCode']?.toString() ?? "", + description: json['description']?.toString() ?? "", + redemptionLink: json['redemptionLink']?.toString() ?? "", + websiteBannerImage: json['websiteBannerImage']?.toString() ?? "", + mobileBannerImage: json['mobileBannerImage']?.toString() ?? "", + passType: json['passType']?.toString() ?? "", + startDateTime: DateTime.tryParse(json['startDateTime'] ?? "") ?? + DateTime.fromMillisecondsSinceEpoch(0), + endDateTime: DateTime.tryParse(json['endDateTime'] ?? "") ?? + DateTime.fromMillisecondsSinceEpoch(0), + offerStatus: json['offerStatus']?.toString() ?? "", + applyToPasses: json['applyToPasses'] ?? false, ); } @@ -186,16 +202,16 @@ class Offer { /// ---------- CARD PASS ---------- class CardPass { - final int id; - final String title; - final String description; - final int validityDuration; - final num adultPrice; // Changed from int to num - final num childPrice; // Changed from int to num - final int minNumber; // ✅ NEW - final int maxNumber; // ✅ NEW - final CardType cardType; - final List offers; + int id; + String title; + String description; + int validityDuration; + num adultPrice; + num childPrice; + int minNumber; + int maxNumber; + CardType cardType; + List offers; CardPass({ required this.id, @@ -210,20 +226,24 @@ class CardPass { required this.offers, }); - factory CardPass.fromJson(Map json) { + factory CardPass.fromJson(Map? json) { + json ??= {}; + return CardPass( - id: json['id'], - title: json['title'], - description: json['description'], - validityDuration: json['validityDuration'], - adultPrice: json['adultPrice'], - childPrice: json['childPrice'], - minNumber: json['minNumber'], // ✅ - maxNumber: json['maxNumber'], // ✅ + id: (json['id'] as num?)?.toInt() ?? 0, + title: json['title']?.toString() ?? "", + description: json['description']?.toString() ?? "", + validityDuration: (json['validityDuration'] as num?)?.toInt() ?? 0, + adultPrice: json['adultPrice'] ?? 0, + childPrice: json['childPrice'] ?? 0, + minNumber: (json['minNumber'] as num?)?.toInt() ?? 0, + maxNumber: (json['maxNumber'] as num?)?.toInt() ?? 0, cardType: CardType.fromJson(json['cardType']), - offers: List.from( - json['offers'].map((x) => Offer.fromJson(x)), - ), + offers: json['offers'] == null + ? [] + : List>.from(json['offers']) + .map((e) => Offer.fromJson(e)) + .toList(), ); } @@ -237,15 +257,15 @@ class CardPass { "minNumber": minNumber, "maxNumber": maxNumber, "cardType": cardType.toJson(), - "offers": offers.map((x) => x.toJson()).toList(), + "offers": offers.map((e) => e.toJson()).toList(), }; } /// ---------- CARD TYPE ---------- class CardType { - final int id; - final String name; - final String displayName; + int id; + String name; + String displayName; CardType({ required this.id, @@ -253,11 +273,13 @@ class CardType { required this.displayName, }); - factory CardType.fromJson(Map json) { + factory CardType.fromJson(Map? json) { + json ??= {}; + return CardType( - id: json['id'], - name: json['name'], - displayName: json['displayName'], + id: (json['id'] as num?)?.toInt() ?? 0, + name: json['name']?.toString() ?? "", + displayName: json['displayName']?.toString() ?? "", ); } @@ -270,27 +292,29 @@ class CardType { /// ---------- ATTRACTION ---------- class Attraction { - final int id; - final String title; - final String slug; - final String thumbnail; - final num? startingFrom; // Changed from int? to num? + int id; + String title; + String slug; + String thumbnail; + num startingFrom; Attraction({ required this.id, required this.title, required this.slug, required this.thumbnail, - this.startingFrom, + required this.startingFrom, }); - factory Attraction.fromJson(Map json) { + factory Attraction.fromJson(Map? json) { + json ??= {}; + return Attraction( - id: json['id'], - title: json['title'], - slug: json['slug'], - thumbnail: json['thumbnail'], - startingFrom: json['startingFrom'], + id: (json['id'] as num?)?.toInt() ?? 0, + title: json['title']?.toString() ?? "", + slug: json['slug']?.toString() ?? "", + thumbnail: json['thumbnail']?.toString() ?? "", + startingFrom: json['startingFrom'] ?? 0, ); } @@ -301,4 +325,4 @@ class Attraction { "thumbnail": thumbnail, "startingFrom": startingFrom, }; -} \ No newline at end of file +} diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart index 9fc53ae..c8db07b 100644 --- a/lib/buy_a_pass/widget/payment_card_view.dart +++ b/lib/buy_a_pass/widget/payment_card_view.dart @@ -95,7 +95,7 @@ class PaymentCard extends StatelessWidget { borderRadius: BorderRadius.circular(20.r), ), child: CustomText( - text: "$cardDisplayName Card", + text: cardDisplayName, size: 12.sp, color: Colors.white, weight: FontWeight.w500, diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart b/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart index 67607d4..81fbe81 100644 --- a/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart +++ b/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart @@ -8,18 +8,122 @@ class MyPassCartBloc extends Bloc { final MyPassCartRepository repository; MyPassCartBloc({required this.repository}) : super(const MyPassCartInitial()) { + on(_onCheckLoginAndFetch); on(_onFetchPassCart); on(_onClearPassCart); } - /// Handle fetching pass cart data + /// Handle checking login status and fetching cart data accordingly + Future _onCheckLoginAndFetch( + CheckLoginAndFetchEvent event, + Emitter emit, + ) async { + try { + if (kDebugMode) { + print('🔍 [BLOC] Checking login status and fetching cart...'); + } + + emit(const MyPassCartLoading()); + + // Check if user is logged in + final isLoggedIn = await repository.isUserLoggedIn(); + + if (kDebugMode) { + print('🔐 [BLOC] User logged in: $isLoggedIn'); + } + + if (isLoggedIn) { + // User is logged in - fetch from API + if (kDebugMode) { + print('🌐 [BLOC] Fetching cart data from API...'); + } + + try { + final apiCartData = await repository.fetchMyPassesCart(); + + // Check if API data is empty + if (apiCartData.cartItems.isEmpty) { + if (kDebugMode) { + print('⚠️ [BLOC] API returned empty cart, checking local data...'); + } + + // Try to fetch from local if API is empty + final localCartData = await repository.fetchPassesCartByLocal(); + + if (localCartData != null) { + if (kDebugMode) { + print('✅ [BLOC] Using local cart data as fallback'); + } + emit(MyPassCartLoaded(cartData: localCartData)); + } else { + if (kDebugMode) { + print('ℹ️ [BLOC] No local data available, cart is empty'); + } + emit(const MyPassCartEmpty()); + } + } else { + // API has cart items + if (kDebugMode) { + print('✅ [BLOC] API cart data loaded successfully with ${apiCartData.cartItems.length} items'); + } + emit(MyPassCartApiLoaded(apiCartData: apiCartData)); + } + } catch (apiError) { + if (kDebugMode) { + print('❌ [BLOC] API error: $apiError, trying local data...'); + } + + // API failed, try local data as fallback + final localCartData = await repository.fetchPassesCartByLocal(); + + if (localCartData != null) { + if (kDebugMode) { + print('✅ [BLOC] Using local cart data after API failure'); + } + emit(MyPassCartLoaded(cartData: localCartData)); + } else { + if (kDebugMode) { + print('❌ [BLOC] No local data available after API failure'); + } + emit(MyPassCartError(message: 'Failed to load cart data: ${apiError.toString()}')); + } + } + } else { + // User is not logged in - fetch from local only + if (kDebugMode) { + print('📱 [BLOC] User not logged in, fetching from local storage...'); + } + + final localCartData = await repository.fetchPassesCartByLocal(); + + if (localCartData != null) { + if (kDebugMode) { + print('✅ [BLOC] Local cart data loaded successfully'); + } + emit(MyPassCartLoaded(cartData: localCartData)); + } else { + if (kDebugMode) { + print('ℹ️ [BLOC] No local cart data available'); + } + emit(const MyPassCartEmpty()); + } + } + } catch (e) { + if (kDebugMode) { + print('❌ [BLOC] Error in CheckLoginAndFetch: $e'); + } + emit(MyPassCartError(message: e.toString())); + } + } + + /// Handle fetching pass cart data from local storage Future _onFetchPassCart( FetchPassCartEvent event, Emitter emit, ) async { try { if (kDebugMode) { - print('🔄 [BLOC] Fetching pass cart...'); + print('📄 [BLOC] Fetching pass cart from local...'); } emit(const MyPassCartLoading()); @@ -52,7 +156,7 @@ class MyPassCartBloc extends Bloc { ) async { try { if (kDebugMode) { - print('🔄 [BLOC] Clearing pass cart...'); + print('📄 [BLOC] Clearing pass cart...'); } // You can add clearPassCart method to repository if needed diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_event.dart b/lib/cart/blocs/myPassCart/my_pass_cart_event.dart index 5bd32ad..da61222 100644 --- a/lib/cart/blocs/myPassCart/my_pass_cart_event.dart +++ b/lib/cart/blocs/myPassCart/my_pass_cart_event.dart @@ -7,6 +7,14 @@ abstract class MyPassCartEvent extends Equatable { List get props => []; } +/// Event to check login status and fetch pass cart data accordingly +/// - If logged in: fetch from API +/// - If not logged in: fetch from local +/// - If API returns empty and local data exists: use local data +class CheckLoginAndFetchEvent extends MyPassCartEvent { + const CheckLoginAndFetchEvent(); +} + /// Event to fetch pass cart data from local database class FetchPassCartEvent extends MyPassCartEvent { const FetchPassCartEvent(); diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_state.dart b/lib/cart/blocs/myPassCart/my_pass_cart_state.dart index 3d6ea24..4dadab0 100644 --- a/lib/cart/blocs/myPassCart/my_pass_cart_state.dart +++ b/lib/cart/blocs/myPassCart/my_pass_cart_state.dart @@ -1,5 +1,7 @@ import 'package:equatable/equatable.dart'; +import '../../model/my_passes_cart_mode.dart'; + abstract class MyPassCartState extends Equatable { const MyPassCartState(); @@ -17,7 +19,7 @@ class MyPassCartLoading extends MyPassCartState { const MyPassCartLoading(); } -/// Loaded state with cart data +/// Loaded state with cart data from local storage class MyPassCartLoaded extends MyPassCartState { final Map cartData; @@ -27,6 +29,16 @@ class MyPassCartLoaded extends MyPassCartState { List get props => [cartData]; } +/// Loaded state with cart data from API +class MyPassCartApiLoaded extends MyPassCartState { + final MyPassesCartModel apiCartData; + + const MyPassCartApiLoaded({required this.apiCartData}); + + @override + List get props => [apiCartData]; +} + /// Empty state when no cart data exists class MyPassCartEmpty extends MyPassCartState { const MyPassCartEmpty(); diff --git a/lib/cart/blocs/pass_bloc.dart b/lib/cart/blocs/pass_bloc.dart index 03b3d02..6356c86 100644 --- a/lib/cart/blocs/pass_bloc.dart +++ b/lib/cart/blocs/pass_bloc.dart @@ -1,40 +1,40 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../model/pass_model.dart'; - -abstract class PassEvent {} -class LoadPasses extends PassEvent {} - -abstract class PassState {} -class PassLoading extends PassState {} -class PassLoaded extends PassState { - final List passes; - final double subtotal; - final double discountPercent; - final double total; - - PassLoaded(this.passes, this.subtotal, this.discountPercent, this.total); -} - -class PassBloc extends Bloc { - PassBloc() : super(PassLoading()) { - on((event, emit) { - final passes = [ - PassModel( - title: "Melbourne", - imageUrl: "assets/images/city_melbourne.png", - duration: "2 days", - adults: 3, - kids: 3, - quantity: 2, - price: 49.50, - discount: 7.2, - ), - ]; - - final subtotal = passes.fold(0.0, (sum, item) => sum + item.price); - final discountPercent = passes.first.discount; - final total = subtotal - (subtotal * discountPercent / 100); - emit(PassLoaded(passes, subtotal, discountPercent, total)); - }); - } -} +// import 'package:flutter_bloc/flutter_bloc.dart'; +// import '../model/pass_model.dart'; +// +// abstract class PassEvent {} +// class LoadPasses extends PassEvent {} +// +// abstract class PassState {} +// class PassLoading extends PassState {} +// class PassLoaded extends PassState { +// final List passes; +// final double subtotal; +// final double discountPercent; +// final double total; +// +// PassLoaded(this.passes, this.subtotal, this.discountPercent, this.total); +// } +// +// class PassBloc extends Bloc { +// PassBloc() : super(PassLoading()) { +// on((event, emit) { +// final passes = [ +// PassModel( +// title: "Melbourne", +// imageUrl: "assets/images/city_melbourne.png", +// duration: "2 days", +// adults: 3, +// kids: 3, +// quantity: 2, +// price: 49.50, +// discount: 7.2, +// ), +// ]; +// +// final subtotal = passes.fold(0.0, (sum, item) => sum + item.price); +// final discountPercent = passes.first.discount; +// final total = subtotal - (subtotal * discountPercent / 100); +// emit(PassLoaded(passes, subtotal, discountPercent, total)); +// }); +// } +// } diff --git a/lib/cart/model/my_passes_cart_mode.dart b/lib/cart/model/my_passes_cart_mode.dart new file mode 100644 index 0000000..ff8eb55 --- /dev/null +++ b/lib/cart/model/my_passes_cart_mode.dart @@ -0,0 +1,207 @@ +import 'dart:convert'; + +/// ---------- MAIN RESPONSE ---------- +MyPassesCartModel myPassesCartModelFromJson(String str) => + MyPassesCartModel.fromJson(json.decode(str)); + +String myPassesCartModelToJson(MyPassesCartModel data) => + json.encode(data.toJson()); + +class MyPassesCartModel { + CartCity city; + List cartItems; + + MyPassesCartModel({ + required this.city, + required this.cartItems, + }); + + factory MyPassesCartModel.fromJson(Map? json) { + json ??= {}; + + return MyPassesCartModel( + city: CartCity.fromJson(json['city']), + cartItems: json['cartItems'] == null + ? [] + : List>.from(json['cartItems']) + .map((e) => CartItem.fromJson(e)) + .toList(), + ); + } + + Map toJson() => { + "city": city.toJson(), + "cartItems": cartItems.map((e) => e.toJson()).toList(), + }; +} + +/// ---------- CITY ---------- +class CartCity { + int id; + String name; + + CartCity({ + required this.id, + required this.name, + }); + + factory CartCity.fromJson(Map? json) { + json ??= {}; + + return CartCity( + id: (json['id'] as num?)?.toInt() ?? 0, + name: json['name']?.toString() ?? "", + ); + } + + Map toJson() => { + "id": id, + "name": name, + }; +} + +/// ---------- CART ITEM ---------- +class CartItem { + int id; + String bookingNumber; + String cardMode; + int noOfDays; + int noOfAttractions; + int totalAdult; + int totalChild; + num baseAmount; + num totalTaxAmount; + num totalAmount; + String bookingStatus; + bool isForSelf; + String recipientFirstName; + String recipientLastName; + String recipientEmail; + String recipientPhone; + String recipientCity; + String recipientCountry; + String giftMessage; + bool isPaymentRequired; + int couponXid; + num couponDiscountAmount; + num couponDiscountPercent; + String paymentStatus; + String createdAt; + ItemCity city; + + CartItem({ + required this.id, + required this.bookingNumber, + required this.cardMode, + required this.noOfDays, + required this.noOfAttractions, + required this.totalAdult, + required this.totalChild, + required this.baseAmount, + required this.totalTaxAmount, + required this.totalAmount, + required this.bookingStatus, + required this.isForSelf, + required this.recipientFirstName, + required this.recipientLastName, + required this.recipientEmail, + required this.recipientPhone, + required this.recipientCity, + required this.recipientCountry, + required this.giftMessage, + required this.isPaymentRequired, + required this.couponXid, + required this.couponDiscountAmount, + required this.couponDiscountPercent, + required this.paymentStatus, + required this.createdAt, + required this.city, + }); + + factory CartItem.fromJson(Map? json) { + json ??= {}; + + return CartItem( + id: (json['id'] as num?)?.toInt() ?? 0, + bookingNumber: json['bookingNumber']?.toString() ?? "", + cardMode: json['cardMode']?.toString() ?? "", + noOfDays: (json['noOfDays'] as num?)?.toInt() ?? 0, + noOfAttractions: (json['noOfAttractions'] as num?)?.toInt() ?? 0, + totalAdult: (json['totalAdult'] as num?)?.toInt() ?? 0, + totalChild: (json['totalChild'] as num?)?.toInt() ?? 0, + baseAmount: json['baseAmount'] ?? 0, + totalTaxAmount: json['totalTaxAmount'] ?? 0, + totalAmount: json['totalAmount'] ?? 0, + bookingStatus: json['bookingStatus']?.toString() ?? "", + isForSelf: json['isForSelf'] ?? false, + recipientFirstName: json['recipientFirstName']?.toString() ?? "", + recipientLastName: json['recipientLastName']?.toString() ?? "", + recipientEmail: json['recipientEmail']?.toString() ?? "", + recipientPhone: json['recipientPhone']?.toString() ?? "", + recipientCity: json['recipientCity']?.toString() ?? "", + recipientCountry: json['recipientCountry']?.toString() ?? "", + giftMessage: json['giftMessage']?.toString() ?? "", + isPaymentRequired: json['isPaymentRequired'] ?? false, + couponXid: (json['couponXid'] as num?)?.toInt() ?? 0, + couponDiscountAmount: json['couponDiscountAmount'] ?? 0, + couponDiscountPercent: json['couponDiscountPercent'] ?? 0, + paymentStatus: json['paymentStatus']?.toString() ?? "", + createdAt: json['createdAt']?.toString() ?? "", + city: ItemCity.fromJson(json['city']), + ); + } + + Map toJson() => { + "id": id, + "bookingNumber": bookingNumber, + "cardMode": cardMode, + "noOfDays": noOfDays, + "noOfAttractions": noOfAttractions, + "totalAdult": totalAdult, + "totalChild": totalChild, + "baseAmount": baseAmount, + "totalTaxAmount": totalTaxAmount, + "totalAmount": totalAmount, + "bookingStatus": bookingStatus, + "isForSelf": isForSelf, + "recipientFirstName": recipientFirstName, + "recipientLastName": recipientLastName, + "recipientEmail": recipientEmail, + "recipientPhone": recipientPhone, + "recipientCity": recipientCity, + "recipientCountry": recipientCountry, + "giftMessage": giftMessage, + "isPaymentRequired": isPaymentRequired, + "couponXid": couponXid, + "couponDiscountAmount": couponDiscountAmount, + "couponDiscountPercent": couponDiscountPercent, + "paymentStatus": paymentStatus, + "createdAt": createdAt, + "city": city.toJson(), + }; +} + +/// ---------- ITEM CITY ---------- +class ItemCity { + int id; + String cityName; + + ItemCity({ + required this.id, + required this.cityName, + }); + + factory ItemCity.fromJson(Map? json) { + json ??= {}; + + return ItemCity( + id: (json['id'] as num?)?.toInt() ?? 0, + cityName: json['cityName']?.toString() ?? "", + ); + } + + Map toJson() => { + "id": id, + "cityName": cityName, + }; +} diff --git a/lib/cart/repository/my_pass_cart_repository.dart b/lib/cart/repository/my_pass_cart_repository.dart index c4b9be3..cab9cbf 100644 --- a/lib/cart/repository/my_pass_cart_repository.dart +++ b/lib/cart/repository/my_pass_cart_repository.dart @@ -1,18 +1,39 @@ import 'package:flutter/foundation.dart'; import '../../localPreference/local_preference.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; +import '../model/my_passes_cart_mode.dart'; class MyPassCartRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Check if user is logged in + Future isUserLoggedIn() async { + try { + final isLogin = await LocalPreference.getLogin(); + if (kDebugMode) { + print('🔐 [REPO] User login status: $isLogin'); + } + return isLogin; + } catch (e) { + if (kDebugMode) { + print('❌ [REPO] Error checking login status: $e'); + } + return false; + } + } /// Fetch pass cart data from local database Future?> fetchPassesCartByLocal() async { try { if (kDebugMode) { - print('🔄 [REPO] Fetching pass cart from local database...'); + print('📄 [REPO] Fetching pass cart from local database...'); } final passCartData = await LocalPreference.getPassCart(); + if (passCartData != null) { if (kDebugMode) { print('✅ [REPO] Pass cart retrieved successfully'); @@ -32,4 +53,31 @@ class MyPassCartRepository { rethrow; } } + + /// Fetch pass cart data from API + Future fetchMyPassesCart() async { + try { + if (kDebugMode) { + print('🌐 [REPO] Fetching pass cart from API...'); + } + + final cityID = await LocalPreference.getSelectedCityId(); + + final response = await _apiService.getApi( + url: '${ApiUrls.myPassesCart}?cityXid=$cityID', + ); + + if (kDebugMode) { + print('✅ [REPO] API response received'); + } + + return MyPassesCartModel.fromJson(response.data); + } catch (e) { + if (kDebugMode) { + print('❌ [REPO] Error fetching pass cart from API: $e'); + } + rethrow; + } + } + } \ No newline at end of file diff --git a/lib/cart/views/my_pass_cart_page_view.dart b/lib/cart/views/my_pass_cart_page_view.dart index 6f8d349..1830026 100644 --- a/lib/cart/views/my_pass_cart_page_view.dart +++ b/lib/cart/views/my_pass_cart_page_view.dart @@ -6,6 +6,8 @@ import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../add_details/add_details_view.dart'; +import '../../checkout/widget/pass_purchase_details_bottomsheet.dart'; import '../../login/view/login_email_bottomsheet.dart'; import '../../common_packages/common_app_texts.dart'; import '../../localPreference/local_preference.dart'; @@ -24,12 +26,13 @@ class _MyPassesPageState extends State { // For coupon/discount management String? appliedCouponCode; double discountPercentage = 0.0; + bool isPurchaseDetailsConfirmed = false; @override void initState() { super.initState(); // Fetch cart data when page loads - context.read().add(const FetchPassCartEvent()); + context.read().add(const CheckLoginAndFetchEvent()); } @override @@ -38,36 +41,42 @@ class _MyPassesPageState extends State { builder: (context, state) { if (state is MyPassCartLoading) { return const Center(child: CircularProgressIndicator()); - } else if (state is MyPassCartLoaded) { - final cartData = state.cartData; + } - // Extract data from cart - final String cityName = cartData['city_name'] as String? ?? ''; - final String heroImage = cartData['hero_image'] as String? ?? ''; - final String cardTypeName = cartData['card_type_name'] as String? ?? ''; - final String cardDisplayName = cartData['card_display_name'] as String? ?? ''; - final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF; - final int adultCount = cartData['adult_count'] as int? ?? 0; - final int childCount = cartData['child_count'] as int? ?? 0; - final double adultPrice = (cartData['adult_price'] as num?)?.toDouble() ?? 0.0; - final double childPrice = (cartData['child_price'] as num?)?.toDouble() ?? 0.0; - final int validityDuration = cartData['validity_duration'] as int? ?? 0; - final double totalPrice = (cartData['total_price'] as num?)?.toDouble() ?? 0.0; - final String? description = cartData['description'] as String?; + // ========== HANDLE API DATA (LOGGED IN USER) ========== + else if (state is MyPassCartApiLoaded) { + final apiCartData = state.apiCartData; + + if (apiCartData.cartItems.isEmpty) { + return const Center(child: Text('Your cart is empty')); + } + + // Get first cart item (you can modify to handle multiple items) + final cartItem = apiCartData.cartItems.first; + + // Extract data from API cart item + final String cityName = cartItem.city.cityName; + final String heroImage = ''; // API doesn't have hero_image + final String cardTypeName = cartItem.cardMode; + final String cardDisplayName = cartItem.cardMode; + final int themeColor = 0xFFF95FAF; + final int adultCount = cartItem.totalAdult; + final int childCount = cartItem.totalChild; + final int validityDuration = cartItem.noOfDays; + final double totalPrice = cartItem.totalAmount.toDouble(); // Calculate pricing - final double subtotal = totalPrice; - final double discountAmount = subtotal * (discountPercentage / 100); - final double taxRate = 0.05; // 5% tax + final double subtotal = cartItem.baseAmount.toDouble(); + final double discountAmount = cartItem.couponDiscountAmount.toDouble(); final double totalBeforeTax = subtotal - discountAmount; - final double taxAmount = totalBeforeTax * taxRate; - final double finalTotal = totalBeforeTax + taxAmount; + final double taxAmount = cartItem.totalTaxAmount.toDouble(); + final double finalTotal = totalPrice; // Determine if unlimited card - final bool isUnlimitedCard = cardTypeName == "unlimited_card"; + final bool isUnlimitedCard = cardTypeName.toLowerCase().contains("unlimited"); final String validityLabel = isUnlimitedCard ? "$validityDuration Days" - : "$validityDuration Attractions"; + : "${cartItem.noOfAttractions} Attractions"; return Column( children: [ @@ -90,23 +99,7 @@ class _MyPassesPageState extends State { topLeft: Radius.circular(8.r), bottomLeft: Radius.circular(8.r), ), - child: heroImage.isNotEmpty - ? Image.network( - heroImage, - width: 105.w, - height: 123.h, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - "assets/images/card_banner.png", - scale: 4, - width: 105.w, - height: 123.h, - fit: BoxFit.cover, - ); - }, - ) - : Image.asset( + child: Image.asset( "assets/images/card_banner.png", scale: 4, width: 105.w, @@ -133,8 +126,460 @@ class _MyPassesPageState extends State { SizedBox( width: MediaQuery.of(context).size.width * .5, child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Image.asset( + 'assets/icons/adult.png', + scale: 4, + ), + SizedBox(width: 4.w), + CustomText( + text: "$adultCount ${adultCount == 1 ? 'adult' : 'adults'}", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + ], + ), + Row( + children: [ + Image.asset( + 'assets/icons/qty.png', + scale: 4, + ), + SizedBox(width: 4.w), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Qty:", + style: TextStyle( + color: Color(0xFF8E8E8E), + fontSize: 12.sp, + ), + ), + TextSpan( + text: " ${adultCount + childCount}", + style: TextStyle( + color: Color(0xFF000000), + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + SizedBox(height: 5.h), + Row( + children: [ + Image.asset( + "assets/icons/kid.png", + scale: 4, + ), + SizedBox(width: 4.w), + CustomText( + text: "$childCount ${childCount == 1 ? 'Kid' : 'Kids'}", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + SizedBox(width: 53.w), + CustomText( + text: "\$${totalPrice.toStringAsFixed(2)}", + size: 24.sp, + weight: FontWeight.w500, + color: Color(0xFFF95F62), + ), + ], + ), + ], + ), + ], + ), + Container( + width: 35.w, + height: 123.h, + decoration: BoxDecoration( + color: Color(themeColor), + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(8.r), + topRight: Radius.circular(8.r), + ), + ), + child: RotatedBox( + quarterTurns: -1, + child: Center( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "$cardDisplayName ", + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: 15.h), + Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + color: Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: Color(0xFFBB474A).withOpacity(0.4), + width: 0.8, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: (cartItem.couponDiscountAmount > 0 || appliedCouponCode != null) + ? "Coupon Applied (${(cartItem.couponDiscountAmount > 0 ? cartItem.couponDiscountPercent : discountPercentage).toStringAsFixed(0)}% off)" + : "Get 10% off on your first trip", + color: Color(0xFF262626), + size: 14.sp, + ), + SizedBox(height: 7.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => AllCouponsBottomsheet(), + ); + }, + child: CustomText( + text: "View all coupons", + color: Color(0xFFF95F62), + size: 12, + ), + ), + SizedBox(width: 3.w), + Icon(Icons.arrow_right, color: Color(0xFFF95F62)), + ], + ), + ], + ), + const Spacer(), + // Only show Apply/Remove button if no API coupon is applied + if (cartItem.couponDiscountAmount == 0) + GestureDetector( + onTap: () { + setState(() { + if (appliedCouponCode == null) { + appliedCouponCode = "FIRST10"; + discountPercentage = 10.0; + } else { + appliedCouponCode = null; + discountPercentage = 0.0; + } + }); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 20.w, + vertical: 10.h, + ), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFFF95F62)), + borderRadius: BorderRadius.circular(8.r), + ), + child: CustomText( + text: appliedCouponCode != null ? "Remove" : "Apply", + color: Color(0xFFF95F62), + size: 14.sp, + ), + ), + ), + ], + ), + ), + SizedBox(height: 15.h), + DashedDivider( + color: Color(0xFFACACAC), + thickness: 1.h, + dashLength: 4, + dashSpace: 4, + ), + SizedBox(height: 10.h), + + // Calculate final discount and totals + Builder( + builder: (context) { + // Use API discount if available, otherwise use local discount + final effectiveDiscountAmount = cartItem.couponDiscountAmount > 0 + ? cartItem.couponDiscountAmount + : (subtotal * (discountPercentage / 100)); + + final effectiveDiscountPercent = cartItem.couponDiscountAmount > 0 + ? cartItem.couponDiscountPercent + : discountPercentage; + + // Calculate tax on subtotal after discount + final subtotalAfterDiscount = subtotal - effectiveDiscountAmount; + final calculatedTax = subtotalAfterDiscount * 0.01; // 1% tax + final calculatedTotal = subtotalAfterDiscount + calculatedTax; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomText(text: "Subtotal", size: 14.sp), + CustomText( + text: "\$${subtotal.toStringAsFixed(2)}", + size: 14.sp, + weight: FontWeight.w500, + ), + ], + ), + SizedBox(height: 14.h), + if (effectiveDiscountAmount > 0) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomText(text: "Discount", size: 14.sp), + CustomText( + text: "-\$${effectiveDiscountAmount.toStringAsFixed(2)} (${effectiveDiscountPercent.toStringAsFixed(0)}%)", + size: 14.sp, + weight: FontWeight.w500, + color: Colors.green, + ), + ], + ), + SizedBox(height: 14.h), + ], + DashedDivider( + color: Color(0xFFACACAC), + thickness: 1.h, + dashLength: 4, + dashSpace: 4, + ), + SizedBox(height: 10.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText(text: 'Total', size: 14.sp), + SizedBox(height: 4.h), + CustomText( + text: "Including \$${calculatedTax.toStringAsFixed(2)} in taxes", + size: 12.sp, + color: Colors.black.withOpacity(0.6), + ), + ], + ), + ), + CustomText( + text: "\$${calculatedTotal.toStringAsFixed(2)}", + size: 24.sp, + weight: FontWeight.w500, + ), + ], + ), + SizedBox(height: 150.h), + FutureBuilder( + future: LocalPreference.getLogin(), + builder: (context, snapshot) { + final isLoggedIn = snapshot.data ?? false; + + return CustomFilledButton( + onTap: () async { + if (isLoggedIn) { + if (isPurchaseDetailsConfirmed) { + print("✅ Ready to pay: \$${calculatedTotal.toStringAsFixed(2)}"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Payment integration pending'), + backgroundColor: Colors.orange, + ), + ); + } else { + final result = await PassPurchaseBottomSheet.show( + context, + bookingId: cartItem.id, + ); + + if (result == 'success') { + setState(() { + isPurchaseDetailsConfirmed = true; + }); + } else if (result == 'gift') { + final giftResult = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => AddDetailsView(bookingId: cartItem.id), + ), + ); + + if (giftResult == 'success') { + setState(() { + isPurchaseDetailsConfirmed = true; + }); + } + } + } + } else { + Navigator.pop(context); + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => const LoginEmailBottomsheet(), + ); + } + }, + width: double.infinity, + label: isLoggedIn + ? (isPurchaseDetailsConfirmed + ? "Pay \$${calculatedTotal.toStringAsFixed(2)}" + : "Checkout") + : "Login to Checkout", + ); + }, + ), + SizedBox(height: 25.h), + ], + ); + }, + ), + ], + ); + } + + // ========== HANDLE LOCAL DATA (NOT LOGGED IN) ========== + else if (state is MyPassCartLoaded) { + final cartData = state.cartData; + + // Extract data from cart + final String cityName = cartData['city_name'] as String? ?? ''; + final String heroImage = cartData['hero_image'] as String? ?? ''; + final String cardTypeName = cartData['card_type_name'] as String? ?? ''; + final String cardDisplayName = cartData['card_display_name'] as String? ?? ''; + final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF; + final int adultCount = cartData['adult_count'] as int? ?? 0; + final int childCount = cartData['child_count'] as int? ?? 0; + final double adultPrice = (cartData['adult_price'] as num?)?.toDouble() ?? 0.0; + final double childPrice = (cartData['child_price'] as num?)?.toDouble() ?? 0.0; + final int validityDuration = cartData['validity_duration'] as int? ?? 0; + final double totalPrice = (cartData['total_price'] as num?)?.toDouble() ?? 0.0; + final String? description = cartData['description'] as String?; + + // Calculate pricing + final double subtotal = totalPrice; + final double discountAmount = subtotal * (discountPercentage / 100); + final double totalBeforeTax = subtotal - discountAmount; + final double taxAmount = 2; + final double finalTotal = totalBeforeTax + taxAmount; + + // Determine if unlimited card + final bool isUnlimitedCard = cardTypeName == "unlimited_card"; + final String validityLabel = isUnlimitedCard + ? "$validityDuration Days" + : "$validityDuration Attractions"; + + return Column( + children: [ + SizedBox(height: 22.h), + Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Color(themeColor).withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.r), + bottomLeft: Radius.circular(8.r), + ), + child: heroImage.isNotEmpty + ? Image.network( + heroImage, + width: 105.w, + height: 123.h, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + "assets/images/card_banner.png", + scale: 4, + width: 105.w, + height: 123.h, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + "assets/images/card_banner.png", + scale: 4, + width: 105.w, + height: 123.h, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 6.66.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: cityName, + weight: FontWeight.w500, + size: 16.sp, + ), + SizedBox(height: 5.h), + CustomText( + text: validityLabel, + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + SizedBox(height: 5.h), + SizedBox( + width: MediaQuery.of(context).size.width * .5, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ @@ -232,13 +677,6 @@ class _MyPassesPageState extends State { fontSize: 16.sp, ), ), - // TextSpan( - // text: "Card", - // style: TextStyle( - // color: Colors.white, - // fontSize: 12.sp, - // ), - // ), ], ), ), @@ -402,42 +840,10 @@ class _MyPassesPageState extends State { ], ), SizedBox(height: 150.h), - - // FutureBuilder for login check - FutureBuilder( - future: LocalPreference.getLogin(), - builder: (context, snapshot) { - final isLoggedIn = snapshot.data ?? false; - - return CustomFilledButton( - onTap: () { - if (!isLoggedIn) { - showModalBottomSheet( - backgroundColor: Colors.white, - context: context, - isScrollControlled: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - builder: (_) => const LoginEmailBottomsheet(), - ); - } else { - // Handle checkout logic for logged in user - // You can navigate to checkout or payment screen - print("✅ User is logged in, proceed to checkout"); - } - }, - width: double.infinity, - label: isLoggedIn ? "Checkout" : "Login to Checkout", - ); - }, - ), - SizedBox(height: 25.h), ], ); - } else if (state is MyPassCartEmpty) { + } + else if (state is MyPassCartEmpty) { return Center( child: Column( children: [ diff --git a/lib/checkout/bloc/checkOut/checkout_bloc.dart b/lib/checkout/bloc/checkOut/checkout_bloc.dart index 0763aa0..f5b6b4e 100644 --- a/lib/checkout/bloc/checkOut/checkout_bloc.dart +++ b/lib/checkout/bloc/checkOut/checkout_bloc.dart @@ -197,6 +197,15 @@ class CheckoutBloc extends Bloc { ConfirmPaymentEvent event, Emitter emit, ) async { + // 🔒 GUARD: Prevent duplicate confirmation calls + if (state is CheckoutCouponsLoadedState) { + final currentState = state as CheckoutCouponsLoadedState; + if (currentState.hasConfirmationBeenSent) { + print('⚠️ [CHECKOUT BLOC] Payment confirmation already sent. Ignoring duplicate call.'); + return; + } + } + // Show loading state if (state is CheckoutCouponsLoadedState) { final currentState = state as CheckoutCouponsLoadedState; @@ -204,6 +213,7 @@ class CheckoutBloc extends Bloc { isConfirmingPayment: true, confirmationError: null, isPaymentConfirmed: false, + hasConfirmationBeenSent: true, // 🔒 Mark as sent )); } else { emit(CheckoutPaymentConfirmingState()); @@ -239,6 +249,7 @@ class CheckoutBloc extends Bloc { isConfirmingPayment: false, isPaymentConfirmed: false, confirmationError: e.toString(), + hasConfirmationBeenSent: false, // 🔓 Reset on error to allow retry )); } else { emit(CheckoutPaymentConfirmationErrorState( diff --git a/lib/checkout/bloc/checkOut/checkout_state.dart b/lib/checkout/bloc/checkOut/checkout_state.dart index f77bc04..fd638a9 100644 --- a/lib/checkout/bloc/checkOut/checkout_state.dart +++ b/lib/checkout/bloc/checkOut/checkout_state.dart @@ -25,6 +25,7 @@ class CheckoutCouponsLoadedState extends CheckoutState { final bool isPaymentConfirmed; final String? confirmationError; final Map? bookingDetails; // Full booking response after confirmation + final bool hasConfirmationBeenSent; // 🔒 Prevent duplicate confirmation calls CheckoutCouponsLoadedState({ required this.coupons, @@ -39,6 +40,7 @@ class CheckoutCouponsLoadedState extends CheckoutState { this.isPaymentConfirmed = false, this.confirmationError, this.bookingDetails, + this.hasConfirmationBeenSent = false, }); CheckoutCouponsLoadedState copyWith({ @@ -56,6 +58,7 @@ class CheckoutCouponsLoadedState extends CheckoutState { String? confirmationError, bool clearClientSecret = false, Map? bookingDetails, + bool? hasConfirmationBeenSent, }) { return CheckoutCouponsLoadedState( coupons: coupons ?? this.coupons, @@ -70,6 +73,7 @@ class CheckoutCouponsLoadedState extends CheckoutState { confirmationError: confirmationError, clientSecret: clearClientSecret ? null : (clientSecret ?? this.clientSecret), bookingDetails: bookingDetails ?? this.bookingDetails, + hasConfirmationBeenSent: hasConfirmationBeenSent ?? this.hasConfirmationBeenSent, ); } } diff --git a/lib/checkout/repository/pass_purchase_details_repository.dart b/lib/checkout/repository/pass_purchase_details_repository.dart index 34826e1..7ee0d5c 100644 --- a/lib/checkout/repository/pass_purchase_details_repository.dart +++ b/lib/checkout/repository/pass_purchase_details_repository.dart @@ -34,12 +34,12 @@ class PassPurchaseDetailsRepository { // Request body final requestBody = { 'isForSelf': isForSelf, - 'recipientName': recipientFirstName ?? '', - // 'recipientLastName': recipientLastName ?? '', + 'recipientFirstName': recipientFirstName ?? '', + 'recipientLastName': recipientLastName ?? '', 'recipientEmail': recipientEmail ?? '', 'recipientPhone': recipientPhone ?? '', - // 'city': city ?? '', - // 'country': country ?? '', + 'recipientCity': city ?? '', + 'recipientCountry': country ?? '', }; log('📦 Request Body: $requestBody'); diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index 9aa119e..18e5f91 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -13,7 +13,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../StripePayment/view/stripe_payment.dart'; import '../../add_details/add_details_view.dart'; import '../../buy_a_pass/models/checkout_model.dart'; +import '../../itinerary_creation/bloc/get_itinerary_bloc.dart'; import '../../localPreference/local_preference.dart'; +import '../../my_pass/blocs/myPasses/my_passes_bloc.dart'; +import '../../my_pass/blocs/myPasses/my_passes_event.dart'; import '../widget/pass_purchase_details_bottomsheet.dart'; import '../repository/all_coupons_repository.dart'; import '../repository/checkout_repository.dart'; @@ -101,7 +104,7 @@ class _CheckoutViewState extends State { } } -class _CheckoutContent extends StatelessWidget { +class _CheckoutContent extends StatefulWidget { final CheckoutData checkoutData; final int bookingId; final bool isPurchaseDetailsConfirmed; @@ -114,6 +117,12 @@ class _CheckoutContent extends StatelessWidget { required this.onPurchaseDetailsChanged, }); + @override + State<_CheckoutContent> createState() => _CheckoutContentState(); +} + +class _CheckoutContentState extends State<_CheckoutContent> { + bool _hasHandledPaymentResult = false; /// 🆕 Handle payment flow with client secret /// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION Future _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async { @@ -165,7 +174,10 @@ class _CheckoutContent extends StatelessWidget { await Future.delayed(const Duration(milliseconds: 500)); // Navigate to home after successful payment + Navigator.of(context).popUntil((route) => route.isFirst); + context.read().add(CheckLoginAndFetchItinerary()); + context.read().add(CheckLoginAndFetchPasses()); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Payment confirmed successfully!'), @@ -181,15 +193,21 @@ class _CheckoutContent extends StatelessWidget { listener: (context, state) { // 🆕 Listen for payment initiation success if (state is CheckoutCouponsLoadedState) { - // Check if clientSecret is available (payment initiated) - if (state.clientSecret != null && state.clientSecret!.isNotEmpty) { + // 🔒 CHECK: Prevent duplicate payment flow initiation + if (state.clientSecret != null && + state.clientSecret!.isNotEmpty && + !_hasHandledPaymentResult) { // 🔒 Only proceed if not already handled + + // 🔒 MARK: Set flag immediately to prevent re-entry + _hasHandledPaymentResult = true; + // ✅ Calculate finalTotal here double discountPercentage = 0.0; if (state.appliedCoupon != null) { discountPercentage = state.appliedCoupon!.discountPercent.toDouble(); } - final num subtotal = checkoutData.totalPrice; + final num subtotal = widget.checkoutData.totalPrice; // Changed to widget. final double discountAmount = subtotal * (discountPercentage / 100); final double totalBeforeTax = subtotal - discountAmount; final double taxAmount = 2; @@ -200,7 +218,7 @@ class _CheckoutContent extends StatelessWidget { _handlePaymentFlow( context, state.clientSecret!, - state.bookingId ?? bookingId, + state.bookingId ?? widget.bookingId, finalTotal, // ✅ Pass the calculated finalTotal ); }); @@ -263,7 +281,7 @@ class _CheckoutContent extends StatelessWidget { isConfirmingPayment = state.isConfirmingPayment; } - final num subtotal = checkoutData.totalPrice; + final num subtotal = widget.checkoutData.totalPrice; final double discountAmount = subtotal * (discountPercentage / 100); // final double taxRate = 0.05; // 5% tax final double totalBeforeTax = subtotal - discountAmount; @@ -307,7 +325,7 @@ class _CheckoutContent extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, border: Border.all( - color: checkoutData.themeColor.withOpacity(0.2), + color: widget.checkoutData.themeColor.withOpacity(0.2), ), borderRadius: BorderRadius.circular(8.r), ), @@ -322,9 +340,9 @@ class _CheckoutContent extends StatelessWidget { topLeft: Radius.circular(8.r), bottomLeft: Radius.circular(8.r), ), - child: checkoutData.heroImage.isNotEmpty + child: widget.checkoutData.heroImage.isNotEmpty ? Image.network( - checkoutData.heroImage, + widget.checkoutData.heroImage, width: 105.w, height: 140.h, fit: BoxFit.cover, @@ -344,7 +362,7 @@ class _CheckoutContent extends StatelessWidget { height: 24.w, child: CircularProgressIndicator( strokeWidth: 2, - color: checkoutData.themeColor, + color: widget.checkoutData.themeColor, ), ), ), @@ -363,7 +381,7 @@ class _CheckoutContent extends StatelessWidget { children: [ // City Name CustomText( - text: checkoutData.cityName, + text: widget.checkoutData.cityName, weight: FontWeight.w500, size: 16.sp, ), @@ -371,7 +389,7 @@ class _CheckoutContent extends StatelessWidget { // Validity (Days or Attractions) CustomText( - text: checkoutData.validityLabel, + text: widget.checkoutData.validityLabel, color: const Color(0xFF8E8E8E), size: 12.sp, ), @@ -385,7 +403,7 @@ class _CheckoutContent extends StatelessWidget { MainAxisAlignment.spaceBetween, children: [ // Adults - if (checkoutData.adultCount > 0) + if (widget.checkoutData.adultCount > 0) Row( children: [ Image.asset( @@ -395,7 +413,7 @@ class _CheckoutContent extends StatelessWidget { SizedBox(width: 4.w), CustomText( text: - "${checkoutData.adultCount} adult${checkoutData.adultCount > 1 ? 's' : ''}", + "${widget.checkoutData.adultCount} adult${widget.checkoutData.adultCount > 1 ? 's' : ''}", color: const Color(0xFF8E8E8E), size: 12.sp, ), @@ -408,7 +426,7 @@ class _CheckoutContent extends StatelessWidget { Row( children: [ // Children - if (checkoutData.childCount > 0) ...[ + if (widget.checkoutData.childCount > 0) ...[ Image.asset( "assets/icons/kid.png", scale: 4, @@ -416,7 +434,7 @@ class _CheckoutContent extends StatelessWidget { SizedBox(width: 4.w), CustomText( text: - "${checkoutData.childCount} Kid${checkoutData.childCount > 1 ? 's' : ''}", + "${widget.checkoutData.childCount} Kid${widget.checkoutData.childCount > 1 ? 's' : ''}", color: const Color(0xFF8E8E8E), size: 12.sp, ), @@ -429,7 +447,7 @@ class _CheckoutContent extends StatelessWidget { text: "\$${subtotal.toStringAsFixed(2)}", size: 24.sp, weight: FontWeight.w500, - color: checkoutData.themeColor, + color: widget.checkoutData.themeColor, ), ], ), @@ -443,7 +461,7 @@ class _CheckoutContent extends StatelessWidget { width: 35.w, height: 140.h, decoration: BoxDecoration( - color: checkoutData.themeColor, + color: widget.checkoutData.themeColor, borderRadius: BorderRadius.only( bottomRight: Radius.circular(8.r), topRight: Radius.circular(8.r), @@ -453,7 +471,7 @@ class _CheckoutContent extends StatelessWidget { quarterTurns: -1, child: Center( child: Text( - checkoutData.cardDisplayName, + widget.checkoutData.cardDisplayName, style: TextStyle( color: Colors.white, fontSize: 14.sp, @@ -550,7 +568,7 @@ class _CheckoutContent extends StatelessWidget { ); context.read().add( ApplyCouponToBackendEvent( - bookingId: bookingId, + bookingId: widget.bookingId, couponCode: coupon.couponCode, ), ); @@ -586,13 +604,13 @@ class _CheckoutContent extends StatelessWidget { onTap: () { if (appliedCoupon != null) { context.read().add( - RemoveCouponEvent(bookingId: bookingId), + RemoveCouponEvent(bookingId: widget.bookingId), ); } else if (state.coupons.isNotEmpty) { // Apply coupon via backend API context.read().add( ApplyCouponToBackendEvent( - bookingId: bookingId, + bookingId: widget.bookingId, couponCode: state.coupons[0].couponCode, ), ); @@ -717,32 +735,32 @@ class _CheckoutContent extends StatelessWidget { ? () {} // Empty callback when disabled : () async { if (isLoggedIn) { - if (isPurchaseDetailsConfirmed) { + if (widget.isPurchaseDetailsConfirmed) { // 🆕 Initiate payment flow context.read().add( InitiatePaymentEvent( - bookingId: bookingId), + bookingId: widget.bookingId), ); } else { // Show purchase details bottom sheet final result = await PassPurchaseBottomSheet.show( - context, bookingId: bookingId); + context, bookingId: widget.bookingId); // ✅ Handle 'Buy for Myself' - user submitted details if (result == 'success') { - onPurchaseDetailsChanged(true); + widget.onPurchaseDetailsChanged(true); } // ✅ Handle 'Gift the Pass' - navigate to AddDetailsView else if (result == 'gift') { final giftResult = await Navigator.of(context).push( MaterialPageRoute( - builder: (_) => AddDetailsView(bookingId: bookingId), + builder: (_) => AddDetailsView(bookingId: widget.bookingId), ), ); // If gift details were successfully submitted, mark as confirmed if (giftResult == 'success') { - onPurchaseDetailsChanged(true); + widget.onPurchaseDetailsChanged(true); } } } @@ -764,7 +782,7 @@ class _CheckoutContent extends StatelessWidget { }, width: double.infinity, label: isLoggedIn - ? (isPurchaseDetailsConfirmed + ? (widget.isPurchaseDetailsConfirmed ? (isInitiatingPayment || isConfirmingPayment ? "Processing..." : "Pay \$${finalTotal.toStringAsFixed(2)}") diff --git a/lib/checkout/widget/pass_purchase_details_bottomsheet.dart b/lib/checkout/widget/pass_purchase_details_bottomsheet.dart index b7c0771..ecee655 100644 --- a/lib/checkout/widget/pass_purchase_details_bottomsheet.dart +++ b/lib/checkout/widget/pass_purchase_details_bottomsheet.dart @@ -47,12 +47,12 @@ class _PassPurchaseContent extends StatelessWidget { Navigator.of(context).pop('success'); // Show success message - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Details submitted successfully!'), - backgroundColor: Color(0xffF95F62), - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Details submitted successfully!'), + // backgroundColor: Color(0xffF95F62), + // ), + // ); } // Handle API submission error diff --git a/lib/common_packages/common_app_texts.dart b/lib/common_packages/common_app_texts.dart index 402c069..af9a14c 100644 --- a/lib/common_packages/common_app_texts.dart +++ b/lib/common_packages/common_app_texts.dart @@ -1,3 +1,3 @@ class CommonAppText { - static const String selectiveCard = "Selective"; + static const String selectiveCard = "Flexi"; } \ No newline at end of file diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index ea01376..bd3df75 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -30,6 +30,10 @@ 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/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart'; +import '../my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart'; +import '../my_pass/repository/my_passes_attractions_repository.dart'; +import '../my_pass/repository/my_passes_offers_repository.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'; @@ -74,8 +78,23 @@ class AppRouter { 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)); + final Map args = settings.arguments as Map; + final int cityId = args['cityId'] as int; + final String source = args['source'] as String; + + return MaterialPageRoute( + builder: (_) { + return BlocProvider( + create: (_) => MyPassesAttractionsBloc( + repository: MyPassesAttractionsRepository(), + ), + child: PassAttractionsPage( + cityXid: cityId, + source: source, + ), + ); + }, + ); case RouteConstants.profile: return MaterialPageRoute( builder: (_) { @@ -205,11 +224,12 @@ class AppRouter { }, ); case RouteConstants.searchPassOffer: + final int cityId = settings.arguments as int; return MaterialPageRoute( builder: (_) { return BlocProvider( - create: (_) => OffersBloc(OffersRepository()), - child: PassOffersScreen(), + create: (_) => MyPassesOffersBloc(MyPassesOffersRepository()), + child: PassOffersScreen(cityId: cityId), ); }, ); diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart index a63601d..cad4181 100644 --- a/lib/core/inside_bottom_navigator.dart +++ b/lib/core/inside_bottom_navigator.dart @@ -19,6 +19,12 @@ import '../itinerary_creation/bloc/itinerary_detail_bloc.dart'; import '../itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; import '../itinerary_creation/views/itinerary_creation_view.dart'; import '../itinerary_creation/views/magic_itinerary_view.dart'; +import '../my_pass/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart'; +import '../my_pass/blocs/myPassesDetails/my_passes_details_bloc.dart'; +import '../my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart'; +import '../my_pass/repository/my_passes_attractions_repository.dart'; +import '../my_pass/repository/my_passes_details_repository.dart'; +import '../my_pass/repository/my_passes_offers_repository.dart'; import '../my_pass/views/booking_page_view.dart'; import '../my_pass/views/booking_successful_page_view.dart'; import '../my_pass/views/pass_details_page_view.dart'; @@ -59,9 +65,22 @@ Widget buildOffstageNavigator( builder: (_) => AttractionsPage(source: args), ); case RouteConstants.passAttractionsPage: - final args = settings.arguments as String; + final Map args = settings.arguments as Map; + final int cityId = args['cityId'] as int; + final String source = args['source'] as String; + return MaterialPageRoute( - builder: (_) => PassAttractionsPage(source: args), + builder: (_) { + return BlocProvider( + create: (_) => MyPassesAttractionsBloc( + repository: MyPassesAttractionsRepository(), + ), + child: PassAttractionsPage( + cityXid: cityId, + source: source, + ), + ); + }, ); case RouteConstants.attractionDetails: @@ -117,11 +136,12 @@ Widget buildOffstageNavigator( }, ); case RouteConstants.searchPassOffer: + final int cityId = settings.arguments as int; return MaterialPageRoute( builder: (_) { return BlocProvider( - create: (_) => OffersBloc(OffersRepository()), - child: PassOffersScreen(), + create: (_) => MyPassesOffersBloc(MyPassesOffersRepository()), + child: PassOffersScreen(cityId: cityId), ); }, ); @@ -157,12 +177,14 @@ Widget buildOffstageNavigator( ); case RouteConstants.qrPage: + final bookingId = settings.arguments as int; return MaterialPageRoute( builder: (context) { - final previousBloc = BlocProvider.of(context); - return BlocProvider.value( - value: previousBloc, - child: const PassDetailsView(), + return BlocProvider( + create: (context) => MyPassesDetailsBloc( + repository: MyPassesDetailsRepository(), + ), + child: PassDetailsView(bookingId: bookingId), ); }, ); diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart index e492639..b68420f 100644 --- a/lib/create_account/view/create_account_view.dart +++ b/lib/create_account/view/create_account_view.dart @@ -6,8 +6,10 @@ 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 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart'; import '../../itinerary_creation/bloc/get_itinerary_bloc.dart'; import '../../localPreference/local_preference.dart'; +import '../../my_pass/blocs/myPasses/my_passes_bloc.dart'; import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart'; import '../../postcard/blocs/myPostCards/my_postcard_event.dart'; import '../../profile/bloc/profile/profile_bloc.dart'; @@ -17,20 +19,26 @@ import '../bloc/create_account_event.dart'; import '../bloc/create_account_state.dart'; import '../repository/create_account_repository.dart'; -class CreateAccountView extends StatelessWidget { +class CreateAccountView extends StatefulWidget { final String email; - CreateAccountView({super.key, required this.email}); + const CreateAccountView({super.key, required this.email}); + @override + State createState() => _CreateAccountViewState(); +} + +class _CreateAccountViewState extends State { final TextEditingController firstNameController = TextEditingController(); final TextEditingController lastNameController = TextEditingController(); 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(); + String? selectedState; + String? selectedCountry; + void _submitForm(BuildContext context) { if (firstNameController.text.trim().isEmpty || lastNameController.text.trim().isEmpty || @@ -38,8 +46,8 @@ class CreateAccountView extends StatelessWidget { phoneController.text.trim().isEmpty || addressController.text.trim().isEmpty || cityController.text.trim().isEmpty || - stateController.text.trim().isEmpty || - countryController.text.trim().isEmpty || + selectedState == null || + selectedCountry == null || postalController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please fill all fields')), @@ -56,16 +64,28 @@ class CreateAccountView extends StatelessWidget { address1: addressController.text.trim(), address2: '', city: cityController.text.trim(), - state: stateController.text.trim(), - country: countryController.text.trim(), + state: selectedState!, + country: selectedCountry!, postalCode: postalController.text.trim(), ), ); } + @override + void dispose() { + firstNameController.dispose(); + lastNameController.dispose(); + emailController.dispose(); + phoneController.dispose(); + addressController.dispose(); + cityController.dispose(); + postalController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - emailController.text = email; + emailController.text = widget.email; return BlocProvider( create: (context) => CreateAccountBloc(repository: CreateAccountRepository()), @@ -81,6 +101,7 @@ class CreateAccountView extends StatelessWidget { // context.read().add(FetchDraftPostCards()); context.read().add(RefreshDraftPostCards()); context.read().add(RefreshOrderPostCards()); + context.read().add(CheckLoginAndFetchPasses()); Navigator.pop(context); ScaffoldMessenger.of( context, @@ -202,22 +223,134 @@ class CreateAccountView extends StatelessWidget { controller: cityController, ), ), + + // State Dropdown Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "State", - hint: "Enter your state", - controller: stateController, + padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText(text: "State", size: 14.sp), + SizedBox(height: 6.h), + Container( + height: 42.h, + padding: EdgeInsets.symmetric(horizontal: 24.w), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: const Color(0xBBC83B61).withOpacity(0.4), + width: 0.4.w, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedState, + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFF8E8E8E), + ), + hint: Text( + "Select state", + style: TextStyle( + fontSize: 12.sp, + color: const Color(0xFF8E8E8E), + ), + ), + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF2D3134), + ), + onChanged: (value) { + setState(() { + selectedState = value; + }); + }, + items: [ + "New South Wales", + "Victoria", + "Queensland", + "South Australia", + "Western Australia", + "Tasmania", + "Northern Territory", + "Australian Capital Territory" + ].map((value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle(fontSize: 14.sp), + ), + ); + }).toList(), + ), + ), + ), + ], ), ), + + // Country Dropdown Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "Country", - hint: "Enter your country", - controller: countryController, + padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText(text: "Country", size: 14.sp), + SizedBox(height: 6.h), + Container( + height: 42.h, + padding: EdgeInsets.symmetric(horizontal: 24.w), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: const Color(0xBBC83B61).withOpacity(0.4), + width: 0.4.w, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedCountry, + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFF8E8E8E), + ), + hint: Text( + "Select country", + style: TextStyle( + fontSize: 12.sp, + color: const Color(0xFF8E8E8E), + ), + ), + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF2D3134), + ), + onChanged: (value) { + setState(() { + selectedCountry = value; + }); + }, + items: ["Australia"].map((value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle(fontSize: 14.sp), + ), + ); + }).toList(), + ), + ), + ), + ], ), ), + Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( @@ -257,4 +390,4 @@ class CreateAccountView extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/home/model/city_selection_model.dart b/lib/home/model/city_selection_model.dart index 35df4fa..ff56ca3 100644 --- a/lib/home/model/city_selection_model.dart +++ b/lib/home/model/city_selection_model.dart @@ -6,7 +6,8 @@ class CitySelectionResponse { factory CitySelectionResponse.fromJson(Map json) { return CitySelectionResponse( cities: (json['cities'] as List?) - ?.map((city) => CitySelection.fromJson(city as Map)) + ?.map((city) => + CitySelection.fromJson(city as Map)) .toList() ?? [], ); @@ -20,33 +21,54 @@ class CitySelectionResponse { } class CitySelection { + // 🔹 EXISTING FIELDS (UNCHANGED) final int id; final String cityName; final String bannerImage; + // 🔹 NEW FIELDS (ADDED ONLY) + final String cityIconPath; + final CityIcon? icon; + CitySelection({ required this.id, required this.cityName, required this.bannerImage, + + // 🔹 ADDED + required this.cityIconPath, + required this.icon, }); factory CitySelection.fromJson(Map json) { return CitySelection( + // 🔹 EXISTING id: json['id'] as int? ?? 0, cityName: json['cityName'] as String? ?? '', bannerImage: json['bannerImage'] as String? ?? '', + + // 🔹 ADDED + cityIconPath: json['cityIconPath'] as String? ?? '', + icon: json['icon'] != null + ? CityIcon.fromJson(json['icon'] as Map) + : null, ); } Map toJson() { return { + // 🔹 EXISTING 'id': id, 'cityName': cityName, 'bannerImage': bannerImage, + + // 🔹 ADDED + 'cityIconPath': cityIconPath, + 'icon': icon?.toJson(), }; } - // Helper method to get the image URL with fallback + // 🔹 EXISTING METHODS (UNCHANGED) String getImageUrl() { if (bannerImage.isEmpty || !bannerImage.startsWith('http')) { return 'assets/images/card_banner.png'; @@ -54,8 +76,26 @@ class CitySelection { return bannerImage; } - // Helper method to check if image is network image bool isNetworkImage() { return bannerImage.isNotEmpty && bannerImage.startsWith('http'); } +} + +// 🔹 NEW MODEL (REQUIRED FOR icon.svg) +class CityIcon { + final String svg; + + CityIcon({required this.svg}); + + factory CityIcon.fromJson(Map json) { + return CityIcon( + svg: json['svg'] as String? ?? '', + ); + } + + Map toJson() { + return { + 'svg': svg, + }; + } } \ No newline at end of file diff --git a/lib/home/widgets/search_city_bottomsheet.dart b/lib/home/widgets/search_city_bottomsheet.dart index d51cb09..f576ade 100644 --- a/lib/home/widgets/search_city_bottomsheet.dart +++ b/lib/home/widgets/search_city_bottomsheet.dart @@ -238,6 +238,7 @@ class _CitySelectionView extends StatelessWidget { city.cityName, city.isNetworkImage(), selectedCityId, + city.cityIconPath, ); }, ); @@ -260,12 +261,15 @@ class _CitySelectionView extends StatelessWidget { String imageUrl, String name, bool isNetwork, - int selectedCityId, // Add this parameter + int selectedCityId, + String? svgIcon, + // Add this parameter ) { final bool isSelected = cityId == selectedCityId; // Check if selected return InkWell( onTap: () async { await LocalPreference.setSelectedCityId(cityId); + await LocalPreference.setSelectedCityLogo(svgIcon!); Navigator.pop(context); context.read().add(FetchHomeData()); debugPrint("Selected City ID: $cityId"); diff --git a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart index bf0cb73..e91a896 100644 --- a/lib/itinerary_creation/bloc/get_itinerary_bloc.dart +++ b/lib/itinerary_creation/bloc/get_itinerary_bloc.dart @@ -1,3 +1,77 @@ +// import 'package:bloc/bloc.dart'; +// import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart'; +// import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart'; +// import 'package:citycards_customer/localPreference/local_preference.dart'; +// import 'package:equatable/equatable.dart'; +// part 'get_itinerary_event.dart'; +// part 'get_itinerary_state.dart'; +// +// class GetItineraryBloc extends Bloc { +// final ItineraryRepository _repository; +// +// GetItineraryBloc({ItineraryRepository? repository}) +// : _repository = repository ?? ItineraryRepository(), +// super(GetItineraryInitial()) { +// on(_onCheckLoginAndFetch); +// on(_onGetItinerary); +// } +// +// Future _onCheckLoginAndFetch( +// CheckLoginAndFetchItinerary event, +// Emitter emit, +// ) async { +// try { +// emit(GetItineraryLoading()); +// +// final isLoggedIn = await LocalPreference.getLogin(); +// +// if (!isLoggedIn) { +// emit(GetItineraryNotLoggedIn()); +// return; +// } +// +// 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( +// error: e.toString().contains('Exception') +// ? e.toString().replaceAll('Exception: ', '') +// : "Failed to load itineraries. Please try again.")); +// } +// } +// +// Future _onGetItinerary( +// GetIiterary event, +// Emitter emit, +// ) async { +// try { +// emit(GetItineraryLoading()); +// +// 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( +// error: e.toString().contains('Exception') +// ? e.toString().replaceAll('Exception: ', '') +// : "Failed to load itineraries. Please try again.")); +// } +// } +// } + import 'package:bloc/bloc.dart'; import 'package:citycards_customer/itinerary_creation/repository/itinerary_repository.dart'; import 'package:citycards_customer/itinerary_creation/models/my_itinerary_model.dart'; @@ -32,13 +106,19 @@ class GetItineraryBloc extends Bloc { final response = await _repository.fetchMyItineraries(); + // Add static itinerary to the list + final itinerariesWithStatic = [ + _createStaticItinerary(), + ...response.itineraries, + ]; + // Check if user has unlimited pass if (!response.isUnlimitedPass) { - emit(GetItineraryRequiresPass(itineraries: response.itineraries)); + emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic)); return; } - emit(GetItinerarySuccessfully(itineraries: response.itineraries)); + emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic)); } catch (e) { emit(GetItineraryFailed( error: e.toString().contains('Exception') @@ -56,13 +136,19 @@ class GetItineraryBloc extends Bloc { final response = await _repository.fetchMyItineraries(); + // Add static itinerary to the list + final itinerariesWithStatic = [ + _createStaticItinerary(), + ...response.itineraries, + ]; + // Check if user has unlimited pass if (!response.isUnlimitedPass) { - emit(GetItineraryRequiresPass(itineraries: response.itineraries)); + emit(GetItineraryRequiresPass(itineraries: itinerariesWithStatic)); return; } - emit(GetItinerarySuccessfully(itineraries: response.itineraries)); + emit(GetItinerarySuccessfully(itineraries: itinerariesWithStatic)); } catch (e) { emit(GetItineraryFailed( error: e.toString().contains('Exception') @@ -70,4 +156,85 @@ class GetItineraryBloc extends Bloc { : "Failed to load itineraries. Please try again.")); } } + + // Helper method to create static/temporary itinerary + MyItinerary _createStaticItinerary() { + return MyItinerary( + id: -1, // Negative ID to identify as static data + userXid: 0, + cityXid: 1, + address: "Sample Location, City Center", + latitude: 40.7128, + longitude: -74.0060, + tripEnergy: "Relaxed", + travelingWithKids: false, + dietaryPreferences: ["Vegetarian"], + preferences: Preferences( + shopping: 3, + wildlife: 2, + landmarks: 5, + scenicViews: 4, + artAndMuseums: 5, + ), + totalDays: 2, + aiModel: "static-v1", + promptVersion: "1.0", + isActive: true, + createdAt: DateTime.now().toIso8601String(), + updatedAt: DateTime.now().toIso8601String(), + days: [ + ItineraryDay( + id: -1, + itineraryXid: -1, + dayNumber: 1, + title: "Day 1: City Exploration", + summary: "Explore the main attractions and local cuisine", + items: [ + DayItem( + id: -1, + itineraryDayXid: -1, + timeSlot: "09:00 AM", + title: "Morning Coffee", + description: "Start your day with a cup of local coffee", + locationName: "Central Cafe", + imageUrl: "https://via.placeholder.com/300", + latitude: 40.7128, + longitude: -74.0060, + ), + DayItem( + id: -2, + itineraryDayXid: -1, + timeSlot: "11:00 AM", + title: "Visit Historic Landmark", + description: "Explore the city's most famous landmark", + locationName: "City Monument", + imageUrl: "https://via.placeholder.com/300", + latitude: 40.7589, + longitude: -73.9851, + ), + ], + ), + ItineraryDay( + id: -2, + itineraryXid: -1, + dayNumber: 2, + title: "Day 2: Museum & Parks", + summary: "Discover art and nature", + items: [ + DayItem( + id: -3, + itineraryDayXid: -2, + timeSlot: "10:00 AM", + title: "Art Museum Visit", + description: "Immerse yourself in contemporary art", + locationName: "Modern Art Museum", + imageUrl: "https://via.placeholder.com/300", + latitude: 40.7614, + longitude: -73.9776, + ), + ], + ), + ], + ); + } } \ No newline at end of file diff --git a/lib/itinerary_creation/views/magic_itinerary_view.dart b/lib/itinerary_creation/views/magic_itinerary_view.dart index 1af2f78..9e255a5 100644 --- a/lib/itinerary_creation/views/magic_itinerary_view.dart +++ b/lib/itinerary_creation/views/magic_itinerary_view.dart @@ -31,7 +31,7 @@ class _MagicItineraryViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Color(0xFFFFF5F5), + backgroundColor: Colors.white, body: SafeArea( child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), @@ -41,7 +41,7 @@ class _MagicItineraryViewState extends State { CommonAppBar( isWhiteLogo: false, isProfilePage: false, - showDivider: false, + showDivider: true, ), SizedBox(height: 24.h), // BLoC Builder for all states diff --git a/lib/localPreference/local_database.dart b/lib/localPreference/local_database.dart index b6e5e2d..b248f02 100644 --- a/lib/localPreference/local_database.dart +++ b/lib/localPreference/local_database.dart @@ -22,14 +22,6 @@ class LocalDatabase { path, version: 1, onCreate: (db, version) async { - /// CITY TABLE - await db.execute(''' - CREATE TABLE selected_city ( - id INTEGER PRIMARY KEY, - city_id INTEGER - ) - '''); - /// ONBOARDING TABLE await db.execute(''' CREATE TABLE onboarding_state ( @@ -90,7 +82,8 @@ class LocalDatabase { description TEXT ) '''); - /// CITY TABLE + + /// CITY TABLE (with city_logo field) await db.execute(''' CREATE TABLE selected_city ( id INTEGER PRIMARY KEY, diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart index 4242dd6..7269a30 100644 --- a/lib/login/view/verify_otp_bottomsheet.dart +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -1,6 +1,8 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/itinerary_creation/bloc/get_itinerary_bloc.dart'; +import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_bloc.dart'; +import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart'; import 'package:citycards_customer/postcard/blocs/myPostCards/my_postcard_bloc.dart'; import 'package:citycards_customer/profile/bloc/profile/profile_bloc.dart'; import 'package:citycards_customer/profile/bloc/profile/profile_event.dart'; @@ -48,6 +50,7 @@ class _VerifyOtpBottomsheetState extends State { context.read().add(RefreshDraftPostCards()); context.read().add(RefreshOrderPostCards()); // context.read().add(FetchOrderPostCards()); + context.read().add(CheckLoginAndFetchPasses()); // User exists - navigate to home/dashboard // Navigator.of(context).pushReplacementNamed(RouteConstants.home); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/main.dart b/lib/main.dart index c1fc509..30028fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:citycards_customer/cart/blocs/postcard_bloc.dart'; +import 'package:citycards_customer/cart/repository/my_pass_cart_repository.dart'; import 'package:citycards_customer/core/route_constants.dart'; import 'package:citycards_customer/search_offers/bloc/offers_bloc.dart'; import 'package:citycards_customer/trail.dart'; @@ -8,6 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; // ADD THIS +import 'cart/blocs/myPassCart/my_pass_cart_bloc.dart'; import 'core/app_router.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart'; @@ -18,7 +20,9 @@ import 'itinerary_creation/bloc/get_itinerary_bloc.dart'; import 'itinerary_creation/views/magic_itinerary_view.dart'; import 'login/bloc/login/login_bloc.dart'; import 'login/repository/login_repository.dart'; +import 'my_pass/blocs/myPasses/my_passes_bloc.dart'; import 'my_pass/blocs/my_pass_bloc.dart'; +import 'my_pass/repository/my_passes_repository.dart'; import 'postcard/blocs/myPostCards/my_postcard_bloc.dart'; import 'postcard/repository/my_postcard_repository.dart'; import 'profile/bloc/profile/profile_bloc.dart'; @@ -58,6 +62,12 @@ class MyApp extends StatelessWidget { BlocProvider( create: (_) => MyPassBloc()..add(LoadMyPasses()), ), + BlocProvider( + create: (_) => MyPassesBloc(MyPassesRepository()), + ), + BlocProvider( + create: (_) => MyPassCartBloc(repository: MyPassCartRepository()), + ), BlocProvider( create: (context) => FirstTimeUserHomeBloc( FirstTimeUserHomeRepository(), diff --git a/lib/my_pass/blocs/myPasses/my_passes_bloc.dart b/lib/my_pass/blocs/myPasses/my_passes_bloc.dart new file mode 100644 index 0000000..27441da --- /dev/null +++ b/lib/my_pass/blocs/myPasses/my_passes_bloc.dart @@ -0,0 +1,85 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../localPreference/local_preference.dart'; +import '../../repository/my_passes_repository.dart'; +import 'my_passes_event.dart'; +import 'my_passes_state.dart'; + +class MyPassesBloc extends Bloc { + final MyPassesRepository repository; + + MyPassesBloc(this.repository) : super(MyPassesInitial()) { + on(_onCheckLoginAndFetchPasses); + on(_onFetchMyPasses); + on(_onRefreshMyPasses); + } + + Future _onCheckLoginAndFetchPasses( + CheckLoginAndFetchPasses event, + Emitter emit, + ) async { + try { + emit(MyPassesLoading()); + + // Check if user is logged in + final isLoggedIn = await LocalPreference.getLogin(); + + if (!isLoggedIn) { + emit(MyPassesNotLoggedIn()); + return; + } + + // User is logged in, fetch passes + final data = await repository.fetchMyPasses( + cardMode: event.cardMode, + sort: event.sort, + ); + + emit(MyPassesLoaded(data)); + } catch (e) { + emit(MyPassesError( + e.toString().contains('Exception') + ? e.toString().replaceAll('Exception: ', '') + : "Failed to load passes. Please try again.")); + } + } + + Future _onFetchMyPasses( + FetchMyPasses event, + Emitter emit, + ) async { + emit(MyPassesLoading()); + + try { + final data = await repository.fetchMyPasses( + cardMode: event.cardMode, + sort: event.sort, + ); + + emit(MyPassesLoaded(data)); + } catch (e) { + emit(MyPassesError( + e.toString().contains('Exception') + ? e.toString().replaceAll('Exception: ', '') + : "Failed to load passes. Please try again.")); + } + } + + Future _onRefreshMyPasses( + RefreshMyPasses event, + Emitter emit, + ) async { + try { + final data = await repository.fetchMyPasses( + cardMode: event.cardMode, + sort: event.sort, + ); + + emit(MyPassesLoaded(data)); + } catch (e) { + emit(MyPassesError( + e.toString().contains('Exception') + ? e.toString().replaceAll('Exception: ', '') + : "Failed to load passes. Please try again.")); + } + } +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPasses/my_passes_event.dart b/lib/my_pass/blocs/myPasses/my_passes_event.dart new file mode 100644 index 0000000..04af4cd --- /dev/null +++ b/lib/my_pass/blocs/myPasses/my_passes_event.dart @@ -0,0 +1,50 @@ +import 'package:equatable/equatable.dart'; + +abstract class MyPassesEvent extends Equatable { + const MyPassesEvent(); + + @override + List get props => []; +} + +/// Check Login and Fetch Passes Event +class CheckLoginAndFetchPasses extends MyPassesEvent { + final String cardMode; + final String sort; + + const CheckLoginAndFetchPasses({ + this.cardMode = "", + this.sort = "", + }); + + @override + List get props => [cardMode, sort]; +} + +/// Initial / Normal Fetch +class FetchMyPasses extends MyPassesEvent { + final String cardMode; + final String sort; + + const FetchMyPasses({ + this.cardMode = "", + this.sort = "", + }); + + @override + List get props => [cardMode, sort]; +} + +/// Refresh Event +class RefreshMyPasses extends MyPassesEvent { + final String cardMode; + final String sort; + + const RefreshMyPasses({ + this.cardMode = "", + this.sort = "", + }); + + @override + List get props => [cardMode, sort]; +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPasses/my_passes_state.dart b/lib/my_pass/blocs/myPasses/my_passes_state.dart new file mode 100644 index 0000000..e660aee --- /dev/null +++ b/lib/my_pass/blocs/myPasses/my_passes_state.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; + +import '../../models/my_passes_model.dart'; + +abstract class MyPassesState extends Equatable { + const MyPassesState(); + + @override + List get props => []; +} + +/// Initial State +class MyPassesInitial extends MyPassesState {} + +/// Loading State +class MyPassesLoading extends MyPassesState {} + +/// Not Logged In State +class MyPassesNotLoggedIn extends MyPassesState {} + +/// Loaded State +class MyPassesLoaded extends MyPassesState { + final MyPassesModel passes; + + const MyPassesLoaded(this.passes); + + @override + List get props => [passes]; +} + +/// Error State +class MyPassesError extends MyPassesState { + final String message; + + const MyPassesError(this.message); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart b/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart new file mode 100644 index 0000000..5fbf157 --- /dev/null +++ b/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_bloc.dart @@ -0,0 +1,72 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../attractions/models/attraction_model.dart'; +import '../../repository/my_passes_attractions_repository.dart'; +import 'my_passes_attractions_event.dart'; +import 'my_passes_attractions_state.dart'; + +class MyPassesAttractionsBloc + extends Bloc { + final MyPassesAttractionsRepository repository; + + MyPassesAttractionsBloc({required this.repository}) + : super(MyPassesAttractionsInitial()) { + on(_onFetchMyPassesAttractionsByCategory); + on(_onSearchMyPassesAttractions); + } + + Future _onFetchMyPassesAttractionsByCategory( + FetchMyPassesAttractionsByCategory event, + Emitter emit, + ) async { + emit(MyPassesAttractionsLoading()); + + try { + final AttractionsResponse response = + await repository.fetchMyPassesAttractions( + cityXid: event.cityXid, + categoryXid: event.categoryXid, // Can be null + ); + + final attractions = response.attractions ?? []; + + emit( + MyPassesAttractionsLoaded( + attractions: attractions, + filteredAttractions: attractions, // Initially show all + categories: response.categories ?? [], + selectedCategoryId: event.categoryXid, // Can be null + searchQuery: '', // Reset search query on category change + ), + ); + } catch (e) { + emit( + MyPassesAttractionsError( + e.toString(), + ), + ); + } + } + + void _onSearchMyPassesAttractions( + SearchMyPassesAttractions event, + Emitter emit, + ) { + final currentState = state; + + if (currentState is MyPassesAttractionsLoaded) { + final query = event.query.toLowerCase(); + + final filtered = currentState.attractions.where((attraction) { + if (query.isEmpty) return true; + return attraction.title?.toLowerCase().contains(query) ?? false; + }).toList(); + + emit( + currentState.copyWith( + filteredAttractions: filtered, + searchQuery: event.query, + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_event.dart b/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_event.dart new file mode 100644 index 0000000..8692230 --- /dev/null +++ b/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_event.dart @@ -0,0 +1,30 @@ +import 'package:equatable/equatable.dart'; + +abstract class MyPassesAttractionsEvent extends Equatable { + const MyPassesAttractionsEvent(); + + @override + List get props => []; +} + +class FetchMyPassesAttractionsByCategory extends MyPassesAttractionsEvent { + final int cityXid; + final int? categoryXid; + + const FetchMyPassesAttractionsByCategory({ + required this.cityXid, + this.categoryXid, + }); + + @override + List get props => [cityXid, categoryXid]; +} + +class SearchMyPassesAttractions extends MyPassesAttractionsEvent { + final String query; + + const SearchMyPassesAttractions(this.query); + + @override + List get props => [query]; +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_state.dart b/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_state.dart new file mode 100644 index 0000000..ca98226 --- /dev/null +++ b/lib/my_pass/blocs/myPassesAttrctions/my_passes_attractions_state.dart @@ -0,0 +1,64 @@ +import 'package:equatable/equatable.dart'; + +import '../../../attractions/models/attraction_model.dart'; + +abstract class MyPassesAttractionsState extends Equatable { + const MyPassesAttractionsState(); + + @override + List get props => []; +} + +class MyPassesAttractionsInitial extends MyPassesAttractionsState {} + +class MyPassesAttractionsLoading extends MyPassesAttractionsState {} + +class MyPassesAttractionsLoaded extends MyPassesAttractionsState { + final List attractions; + final List filteredAttractions; + final List categories; + final int? selectedCategoryId; + final String searchQuery; + + const MyPassesAttractionsLoaded({ + required this.attractions, + required this.filteredAttractions, + required this.categories, + this.selectedCategoryId, + this.searchQuery = '', + }); + + MyPassesAttractionsLoaded copyWith({ + List? attractions, + List? filteredAttractions, + List? categories, + int? selectedCategoryId, + String? searchQuery, + }) { + return MyPassesAttractionsLoaded( + attractions: attractions ?? this.attractions, + filteredAttractions: filteredAttractions ?? this.filteredAttractions, + categories: categories ?? this.categories, + selectedCategoryId: selectedCategoryId ?? this.selectedCategoryId, + searchQuery: searchQuery ?? this.searchQuery, + ); + } + + @override + List get props => [ + attractions, + filteredAttractions, + categories, + selectedCategoryId, + searchQuery, + ]; +} + +class MyPassesAttractionsError extends MyPassesAttractionsState { + final String message; + + const MyPassesAttractionsError(this.message); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesDetails/my_passes_details_bloc.dart b/lib/my_pass/blocs/myPassesDetails/my_passes_details_bloc.dart new file mode 100644 index 0000000..15bf417 --- /dev/null +++ b/lib/my_pass/blocs/myPassesDetails/my_passes_details_bloc.dart @@ -0,0 +1,30 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/my_passes_details_repository.dart'; +import 'my_passes_details_event.dart'; +import 'my_passes_details_state.dart'; + +class MyPassesDetailsBloc + extends Bloc { + final MyPassesDetailsRepository repository; + + MyPassesDetailsBloc({required this.repository}) + : super(MyPassesDetailsInitial()) { + on(_fetchPassDetails); + } + + Future _fetchPassDetails( + FetchMyPassDetails event, + Emitter emit, + ) async { + emit(MyPassesDetailsLoading()); + + try { + final response = + await repository.fetchPassDetails(passId: event.passId); + + emit(MyPassesDetailsLoaded(data: response)); + } catch (e) { + emit(MyPassesDetailsError(message: e.toString())); + } + } +} diff --git a/lib/my_pass/blocs/myPassesDetails/my_passes_details_event.dart b/lib/my_pass/blocs/myPassesDetails/my_passes_details_event.dart new file mode 100644 index 0000000..62e614c --- /dev/null +++ b/lib/my_pass/blocs/myPassesDetails/my_passes_details_event.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; + +abstract class MyPassesDetailsEvent extends Equatable { + const MyPassesDetailsEvent(); + + @override + List get props => []; +} + +class FetchMyPassDetails extends MyPassesDetailsEvent { + final int passId; + + const FetchMyPassDetails({required this.passId}); + + @override + List get props => [passId]; +} diff --git a/lib/my_pass/blocs/myPassesDetails/my_passes_details_state.dart b/lib/my_pass/blocs/myPassesDetails/my_passes_details_state.dart new file mode 100644 index 0000000..b3de1a1 --- /dev/null +++ b/lib/my_pass/blocs/myPassesDetails/my_passes_details_state.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +import '../../models/my_passes_details_model.dart'; + +abstract class MyPassesDetailsState extends Equatable { + const MyPassesDetailsState(); + + @override + List get props => []; +} + +class MyPassesDetailsInitial extends MyPassesDetailsState {} + +class MyPassesDetailsLoading extends MyPassesDetailsState {} + +class MyPassesDetailsLoaded extends MyPassesDetailsState { + final MyPassesDetailsModel data; + + const MyPassesDetailsLoaded({required this.data}); + + @override + List get props => [data]; +} + +class MyPassesDetailsError extends MyPassesDetailsState { + final String message; + + const MyPassesDetailsError({required this.message}); + + @override + List get props => [message]; +} diff --git a/lib/my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart b/lib/my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart new file mode 100644 index 0000000..0fec690 --- /dev/null +++ b/lib/my_pass/blocs/myPassesOffers/my_passes_offers_bloc.dart @@ -0,0 +1,67 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../search_offers/model/offers_model.dart'; +import '../../repository/my_passes_offers_repository.dart'; +import 'my_passes_offers_event.dart'; +import 'my_passes_offers_state.dart'; + +class MyPassesOffersBloc extends Bloc { + final MyPassesOffersRepository repository; + + List _allOffers = []; + + MyPassesOffersBloc(this.repository) : super(MyPassesOffersInitial()) { + on(_onLoadMyPassesOffers); + on(_onSearchMyPassesOffers); + } + + Future _onLoadMyPassesOffers( + LoadMyPassesOffers event, + Emitter emit, + ) async { + emit(MyPassesOffersLoading()); + + try { + final response = await repository.fetchMyPassesOffers( + cityXid: event.cityXid, + categoryXid: event.categoryXid, + ); + + _allOffers = response.offers; + + emit( + MyPassesOffersLoaded( + offers: response.offers, + categories: response.categories, + ), + ); + } catch (e) { + emit(MyPassesOffersError(e.toString())); + } + } + + void _onSearchMyPassesOffers( + SearchMyPassesOffers event, + Emitter emit, + ) { + final filtered = _allOffers + .where( + (offer) => + offer.title + .toLowerCase() + .contains(event.query.toLowerCase()) || + offer.description + .toLowerCase() + .contains(event.query.toLowerCase()), + ) + .toList(); + + if (state is MyPassesOffersLoaded) { + emit( + MyPassesOffersLoaded( + offers: filtered, + categories: (state as MyPassesOffersLoaded).categories, + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesOffers/my_passes_offers_event.dart b/lib/my_pass/blocs/myPassesOffers/my_passes_offers_event.dart new file mode 100644 index 0000000..d985d51 --- /dev/null +++ b/lib/my_pass/blocs/myPassesOffers/my_passes_offers_event.dart @@ -0,0 +1,16 @@ +abstract class MyPassesOffersEvent {} + +class LoadMyPassesOffers extends MyPassesOffersEvent { + final int cityXid; + final int? categoryXid; + + LoadMyPassesOffers({ + required this.cityXid, + this.categoryXid, + }); +} + +class SearchMyPassesOffers extends MyPassesOffersEvent { + final String query; + SearchMyPassesOffers(this.query); +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesOffers/my_passes_offers_state.dart b/lib/my_pass/blocs/myPassesOffers/my_passes_offers_state.dart new file mode 100644 index 0000000..554b5f9 --- /dev/null +++ b/lib/my_pass/blocs/myPassesOffers/my_passes_offers_state.dart @@ -0,0 +1,22 @@ +import '../../../search_offers/model/offers_model.dart'; + +abstract class MyPassesOffersState {} + +class MyPassesOffersInitial extends MyPassesOffersState {} + +class MyPassesOffersLoading extends MyPassesOffersState {} + +class MyPassesOffersLoaded extends MyPassesOffersState { + final List offers; + final List categories; + + MyPassesOffersLoaded({ + required this.offers, + required this.categories, + }); +} + +class MyPassesOffersError extends MyPassesOffersState { + final String message; + MyPassesOffersError(this.message); +} \ No newline at end of file diff --git a/lib/my_pass/models/my_passes_details_model.dart b/lib/my_pass/models/my_passes_details_model.dart new file mode 100644 index 0000000..c2f45fa --- /dev/null +++ b/lib/my_pass/models/my_passes_details_model.dart @@ -0,0 +1,167 @@ +class MyPassesDetailsModel { + final City? city; + final List attractions; + final List offers; + + MyPassesDetailsModel({ + this.city, + required this.attractions, + required this.offers, + }); + + factory MyPassesDetailsModel.fromJson(Map? json) { + return MyPassesDetailsModel( + city: json?['city'] != null + ? City.fromJson(json?['city']) + : null, + attractions: (json?['attractions'] as List?) + ?.map((e) => Attraction.fromJson(e)) + .toList() ?? + [], + offers: (json?['offers'] as List?) + ?.map((e) => Offer.fromJson(e)) + .toList() ?? + [], + ); + } + + Map toJson() { + return { + 'city': city?.toJson(), + 'attractions': attractions.map((e) => e.toJson()).toList(), + 'offers': offers.map((e) => e.toJson()).toList(), + }; + } +} + +class City { + final num id; + final String name; + final String cardMode; + final String validUpto; + final num totalAdult; + final num totalChild; + final num noOfDays; + final num noOfAttractions; + + City({ + required this.id, + required this.name, + required this.cardMode, + required this.validUpto, + required this.totalAdult, + required this.totalChild, + required this.noOfDays, + required this.noOfAttractions, + }); + + factory City.fromJson(Map? json) { + return City( + id: json?['id'] ?? 0, + name: json?['name'] ?? '', + cardMode: json?['cardMode'] ?? '', + validUpto: json?['validUpto'] ?? '', + totalAdult: json?['totalAdult'] ?? 0, + totalChild: json?['totalChild'] ?? 0, + noOfDays: json?['noOfDays'] ?? 0, + noOfAttractions: json?['noOfAttractions'] ?? 0, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'cardMode': cardMode, + 'validUpto': validUpto, + 'totalAdult': totalAdult, + 'totalChild': totalChild, + 'noOfDays': noOfDays, + 'noOfAttractions': noOfAttractions, + }; + } +} + +class Attraction { + final num id; + final String title; + final String description; + final num? ticketPriceAdult; + final num? ticketPriceChild; + final String? bookingEmail; + final String? bookingPhoneNumber; + final String image; + + Attraction({ + required this.id, + required this.title, + required this.description, + this.ticketPriceAdult, + this.ticketPriceChild, + this.bookingEmail, + this.bookingPhoneNumber, + required this.image, + }); + + factory Attraction.fromJson(Map? json) { + return Attraction( + id: json?['id'] ?? 0, + title: json?['title'] ?? '', + description: json?['description'] ?? '', + ticketPriceAdult: json?['ticketPriceAdult'], + ticketPriceChild: json?['ticketPriceChild'], + bookingEmail: json?['bookingEmail'], + bookingPhoneNumber: json?['bookingPhoneNumber'], + image: json?['image'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'ticketPriceAdult': ticketPriceAdult, + 'ticketPriceChild': ticketPriceChild, + 'bookingEmail': bookingEmail, + 'bookingPhoneNumber': bookingPhoneNumber, + 'image': image, + }; + } +} + +class Offer { + final num id; + final String title; + final String description; + final String mobileBannerImage; + final String websiteBannerImage; + + Offer({ + required this.id, + required this.title, + required this.description, + required this.mobileBannerImage, + required this.websiteBannerImage, + }); + + factory Offer.fromJson(Map? json) { + return Offer( + id: json?['id'] ?? 0, + title: json?['title'] ?? '', + description: json?['description'] ?? '', + mobileBannerImage: json?['mobileBannerImage'] ?? '', + websiteBannerImage: json?['websiteBannerImage'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'mobileBannerImage': mobileBannerImage, + 'websiteBannerImage': websiteBannerImage, + }; + } +} diff --git a/lib/my_pass/models/my_passes_model.dart b/lib/my_pass/models/my_passes_model.dart new file mode 100644 index 0000000..9d8678f --- /dev/null +++ b/lib/my_pass/models/my_passes_model.dart @@ -0,0 +1,119 @@ +class MyPassesModel { + final List? data; + + MyPassesModel({ + this.data, + }); + + factory MyPassesModel.fromJson(List? json) { + return MyPassesModel( + data: json != null + ? json.map((e) => MyPassData.fromJson(e)).toList() + : [], + ); + } + + List toJson() { + return data != null + ? data!.map((e) => e.toJson()).toList() + : []; + } +} + +class MyPassData { + final num? id; + final String? bookingNumber; + final String? cardMode; + final String? validUpto; + final num? totalAdult; + final num? totalChild; + final num? totalAmount; + final String? bookingStatus; + final num? noOfAttractions; + final num? noOfDays; + final String? paymentStatus; + final String? updatedAt; + final City? city; + + MyPassData({ + this.id, + this.bookingNumber, + this.cardMode, + this.validUpto, + this.totalAdult, + this.totalChild, + this.totalAmount, + this.bookingStatus, + this.noOfAttractions, + this.noOfDays, + this.paymentStatus, + this.updatedAt, + this.city, + }); + + factory MyPassData.fromJson(Map? json) { + return MyPassData( + id: json?['id'] ?? 0, + bookingNumber: json?['bookingNumber'] ?? '', + cardMode: json?['cardMode'] ?? '', + validUpto: json?['validUpto'] ?? '', + totalAdult: json?['totalAdult'] ?? 0, + totalChild: json?['totalChild'] ?? 0, + totalAmount: json?['totalAmount'] ?? 0, + bookingStatus: json?['bookingStatus'] ?? '', + noOfAttractions: json?['noOfAttractions'] ?? 0, + noOfDays: json?['noOfDays'] ?? 0, + paymentStatus: json?['paymentStatus'] ?? '', + updatedAt: json?['updatedAt'] ?? '', + city: json?['city'] != null + ? City.fromJson(json?['city']) + : null, + ); + } + + Map toJson() { + return { + 'id': id ?? 0, + 'bookingNumber': bookingNumber ?? '', + 'cardMode': cardMode ?? '', + 'validUpto': validUpto ?? '', + 'totalAdult': totalAdult ?? 0, + 'totalChild': totalChild ?? 0, + 'totalAmount': totalAmount ?? 0, + 'bookingStatus': bookingStatus ?? '', + 'noOfAttractions': noOfAttractions ?? 0, + 'noOfDays': noOfDays ?? 0, + 'paymentStatus': paymentStatus ?? '', + 'updatedAt': updatedAt ?? '', + 'city': city?.toJson(), + }; + } +} + +class City { + final num? id; + final String? name; + final String? bannerImage; + + City({ + this.id, + this.name, + this.bannerImage, + }); + + factory City.fromJson(Map? json) { + return City( + id: json?['id'] ?? 0, + name: json?['name'] ?? '', + bannerImage: json?['bannerImage'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id ?? 0, + 'name': name ?? '', + 'bannerImage': bannerImage ?? '', + }; + } +} diff --git a/lib/my_pass/repository/my_passes_attractions_repository.dart b/lib/my_pass/repository/my_passes_attractions_repository.dart new file mode 100644 index 0000000..52ff59e --- /dev/null +++ b/lib/my_pass/repository/my_passes_attractions_repository.dart @@ -0,0 +1,29 @@ +import 'package:citycards_customer/networkApiServices/api_urls.dart'; +import '../../attractions/models/attraction_model.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class MyPassesAttractionsRepository { + final NetworkApiService _apiServices = NetworkApiService(); + + /// Fetch my passes attractions by cityXid and optional categoryXid + Future fetchMyPassesAttractions({ + required int cityXid, + int? categoryXid, + }) async { + try { + // Base URL + String url = '${ApiUrls.passAttractionsList}?cityXid=$cityXid'; + + // Add categoryXid if provided + if (categoryXid != null) { + url = '$url&categoryXid=$categoryXid'; + } + + final response = await _apiServices.getApi(url: url); + + return AttractionsResponse.fromJson(response.data); + } catch (e) { + throw Exception('Failed to fetch my passes attractions: $e'); + } + } +} \ No newline at end of file diff --git a/lib/my_pass/repository/my_passes_details_repository.dart b/lib/my_pass/repository/my_passes_details_repository.dart new file mode 100644 index 0000000..dfa1b0b --- /dev/null +++ b/lib/my_pass/repository/my_passes_details_repository.dart @@ -0,0 +1,18 @@ +import '../models/my_passes_details_model.dart'; +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; + +class MyPassesDetailsRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Fetch pass details by passId + Future fetchPassDetails({ + required int passId, + }) async { + final response = await _apiService.getApi( + url: '${ApiUrls.passDetails}/$passId/details', + ); + + return MyPassesDetailsModel.fromJson(response.data); + } +} diff --git a/lib/my_pass/repository/my_passes_offers_repository.dart b/lib/my_pass/repository/my_passes_offers_repository.dart new file mode 100644 index 0000000..c1cda32 --- /dev/null +++ b/lib/my_pass/repository/my_passes_offers_repository.dart @@ -0,0 +1,25 @@ +import '../../networkApiServices/api_urls.dart'; +import '../../search_offers/model/offers_model.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class MyPassesOffersRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Fetch my passes offers by cityXid and optionally by categoryXid + Future fetchMyPassesOffers({ + required int cityXid, + int? categoryXid, + }) async { + String url = '${ApiUrls.passOffers}?cityXid=$cityXid'; + + if (categoryXid != null) { + url += '&categoryXid=$categoryXid'; + } + + final response = await _apiService.getApi( + url: url, + ); + + return OffersResponse.fromJson(response.data); + } +} \ No newline at end of file diff --git a/lib/my_pass/repository/my_passes_repository.dart b/lib/my_pass/repository/my_passes_repository.dart new file mode 100644 index 0000000..8c3e27e --- /dev/null +++ b/lib/my_pass/repository/my_passes_repository.dart @@ -0,0 +1,32 @@ +import '../models/my_passes_model.dart'; +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; + +class MyPassesRepository { + final NetworkApiService _apiService = NetworkApiService(); + + Future fetchMyPasses({ + String cardMode = "", + String sort = "", + }) async { + String url = ApiUrls.myPasses; + + List queryParams = []; + + if (cardMode.isNotEmpty) { + queryParams.add("cardMode=$cardMode"); + } + + if (sort.isNotEmpty) { + queryParams.add("sort=$sort"); + } + + if (queryParams.isNotEmpty) { + url += "?${queryParams.join("&")}"; + } + + final response = await _apiService.getApi(url: url); + + return MyPassesModel.fromJson(response.data); + } +} diff --git a/lib/my_pass/views/my_pass_page_view.dart b/lib/my_pass/views/my_pass_page_view.dart index 60843bf..a7bcdcd 100644 --- a/lib/my_pass/views/my_pass_page_view.dart +++ b/lib/my_pass/views/my_pass_page_view.dart @@ -3,78 +3,337 @@ 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_bloc/bottom_navigation_bloc.dart'; +import '../../common_packages/custom_filled_button.dart'; import '../../core/route_constants.dart'; -import '../blocs/my_pass_bloc.dart'; +import '../../login/view/login_email_bottomsheet.dart'; +import '../blocs/myPasses/my_passes_bloc.dart'; +import '../blocs/myPasses/my_passes_event.dart'; +import '../blocs/myPasses/my_passes_state.dart'; import '../widgets/pass_widget.dart'; -class MyPassesView extends StatelessWidget { +class MyPassesView extends StatefulWidget { const MyPassesView({super.key}); @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is MyPassLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is MyPassEmpty) { - return _noPassView(context); - } else if (state is MyPassLoaded) { - return _passListView(state.passes); - } - return const SizedBox.shrink(); - }, + State createState() => _MyPassesViewState(); +} + +class _MyPassesViewState extends State { + String selectedCardMode = ""; + String selectedSort = ""; + + @override + void initState() { + super.initState(); + // Changed from FetchMyPasses to CheckLoginAndFetchPasses + context.read().add(const CheckLoginAndFetchPasses()); + } + + void _showCardModeBottomSheet() { + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), + ), + builder: (context) { + return Container( + padding: EdgeInsets.all(16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + "All", + style: GoogleFonts.poppins(fontSize: 14.sp), + ), + onTap: () { + setState(() { + selectedCardMode = ""; + }); + Navigator.pop(context); + context.read().add(FetchMyPasses( + cardMode: "", + sort: selectedSort, + )); + }, + ), + ListTile( + title: Text( + "flexi", + style: GoogleFonts.poppins(fontSize: 14.sp), + ), + onTap: () { + setState(() { + selectedCardMode = "flexi"; + }); + Navigator.pop(context); + context.read().add(FetchMyPasses( + cardMode: "flexi", + sort: selectedSort, + )); + }, + ), + ListTile( + title: Text( + "unlimited", + style: GoogleFonts.poppins(fontSize: 14.sp), + ), + onTap: () { + setState(() { + selectedCardMode = "unlimited"; + }); + Navigator.pop(context); + context.read().add(FetchMyPasses( + cardMode: "unlimited", + sort: selectedSort, + )); + }, + ), + ], + ), + ); + }, ); } - Widget _noPassView(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 30.h), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/no_pass.png', // your woman sitting image - height: 180.h, - ), - SizedBox(height: 20.h), - Text( - "You Don’t have a Pass Yet! 😕", - style: GoogleFonts.poppins( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 8.h), - Text( - "Get a pass and get offers and discounts and\nmore on your trip to your favourite city", - style: GoogleFonts.poppins(fontSize: 12.sp, color: Colors.black54), - textAlign: TextAlign.center, - ), - SizedBox(height: 24.h), - GestureDetector( - onTap: () { - // Navigate to Buy a Pass - Navigator.pushNamed(context, '/buyPass'); - }, - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 14.h), - decoration: BoxDecoration( - color: const Color(0xffFF5A5F), - borderRadius: BorderRadius.circular(30.r), + void _showSortBottomSheet() { + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), + ), + builder: (context) { + return Container( + padding: EdgeInsets.all(16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + "All", + style: GoogleFonts.poppins(fontSize: 14.sp), + ), + onTap: () { + setState(() { + selectedSort = ""; + }); + Navigator.pop(context); + context.read().add(FetchMyPasses( + cardMode: selectedCardMode, + sort: "", + )); + }, ), - child: Center( + ListTile( + title: Text( + "latest", + style: GoogleFonts.poppins(fontSize: 14.sp), + ), + onTap: () { + setState(() { + selectedSort = "latest"; + }); + Navigator.pop(context); + context.read().add(FetchMyPasses( + cardMode: selectedCardMode, + sort: "latest", + )); + }, + ), + ListTile( + title: Text( + "oldest", + style: GoogleFonts.poppins(fontSize: 14.sp), + ), + onTap: () { + setState(() { + selectedSort = "oldest"; + }); + Navigator.pop(context); + context.read().add(FetchMyPasses( + cardMode: selectedCardMode, + sort: "oldest", + )); + }, + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state is MyPassesLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is MyPassesNotLoggedIn) { + // New state handling for not logged in users + return _notLoggedInView(context); + } else if (state is MyPassesLoaded) { + return SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + SizedBox(height: 10.h), + Row( + children: [ + GestureDetector( + onTap: _showSortBottomSheet, + child: Container( + width: 130.w, + height: 36.h, + padding: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: const Color(0xffFEE7E7), + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: const Color(0xffFDCDCE)), + ), + child: Row( + children: [ + Text( + selectedSort.isEmpty ? "Sort by Date" : selectedSort, + style: GoogleFonts.poppins(fontSize: 12.sp), + ), + const Spacer(), + const Icon(Icons.sort, size: 16), + ], + ), + ), + ), + SizedBox(width: 10.w), + GestureDetector( + onTap: _showCardModeBottomSheet, + child: Container( + height: 36.h, + width: 130.w, + padding: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: const Color(0xffFEE7E7), + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: const Color(0xffFDCDCE)), + ), + child: Row( + children: [ + Text( + selectedCardMode.isEmpty ? "All" : selectedCardMode, + style: GoogleFonts.poppins(fontSize: 12.sp), + ), + const Spacer(), + const Icon(Icons.keyboard_arrow_down_rounded, size: 18), + ], + ), + ), + ), + ], + ), + SizedBox(height: 20.h), + if (state.passes.data == null || state.passes.data!.isEmpty) + _noPassView(context) + else + _passListView(state.passes.data!), + ], + ), + ); + } else if (state is MyPassesError) { + return Center( child: Text( - "Buy a Pass", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, + state.message, + style: GoogleFonts.poppins(fontSize: 14.sp, color: Colors.red), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ); + } + + /// New widget for not logged in state + Widget _notLoggedInView(BuildContext context) { + return SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + SizedBox(height: 40.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: Column( + children: [ + /// Illustration Image + Center( + child: Image.asset( + "assets/images/no_itinerary.png", // You can use a different image if available + height: 260.h, + fit: BoxFit.contain, ), ), - ), + + SizedBox(height: 32.h), + + /// Title + Text( + "Please Log In to View Your Passes", + style: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: 12.h), + + /// Subtitle + Text( + "Log in to access your passes, exclusive offers, and discounts on your trip to your favourite city.", + style: GoogleFonts.poppins( + fontSize: 14.sp, + color: const Color(0xFF656565), + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: 32.h), + + /// Login Button + CustomFilledButton( + onTap: () { + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)), + ), + builder: (_) => const LoginEmailBottomsheet(), + ); + }, + label: "Log In", + showArrow: true, + ), + + SizedBox(height: 40.h), + ], ), ), ], @@ -82,87 +341,84 @@ class MyPassesView extends StatelessWidget { ); } - Widget _passListView(List passes) { - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: SingleChildScrollView( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - SizedBox(height: 10.h), - Row( - children: [ - Container( - width: 130.w, - height: 36.h, - padding: EdgeInsets.symmetric(horizontal: 12.w), - decoration: BoxDecoration( - color: const Color(0xffFEE7E7), - borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: const Color(0xffFDCDCE)), - ), - child: Row( - children: [ - Text( - "Sort by Date", - style: GoogleFonts.poppins(fontSize: 12.sp), - ), - const Spacer(), - const Icon(Icons.sort, size: 16), - ], - ), - ), - SizedBox(width: 10.w), - Container( - height: 36.h, - width: 130.w, - padding: EdgeInsets.symmetric(horizontal: 12.w), - decoration: BoxDecoration( - color: const Color(0xffFEE7E7), - borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: const Color(0xffFDCDCE)), - ), - child: Row( - children: [ - Text( - "All", - style: GoogleFonts.poppins(fontSize: 12.sp), - ), - const Spacer(), - const Icon(Icons.keyboard_arrow_down_rounded, size: 18), - ], - ), - ), - ], - ), - SizedBox(height: 20.h), - ListView.builder( - itemCount: passes.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - final pass = passes[index]; - return Padding( - padding: EdgeInsets.only(bottom: 16.h), - child: InkWell( - onTap: (){ - context.read().add(SelectPass(pass)); - Navigator.of( - context, - ).pushNamed(RouteConstants.qrPage); - }, - child: PassTicketCard(pass: pass), - ), - ); - }, - ), - ], + Widget _noPassView(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 + Text( + "You Don't have a Pass Yet! 😕", + style: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: 12.h), + + /// Subtitle + Text( + "Get a pass and unlock exclusive offers, discounts, and more on your trip to your favourite city.", + style: GoogleFonts.poppins( + fontSize: 14.sp, + color: const Color(0xFF656565), + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: 32.h), + + /// Custom Filled Button + CustomFilledButton( + onTap: () { + context.read().add(NavigationTabChanged(0)); + }, + label: "Buy a Pass", + showArrow: true, + ), + + SizedBox(height: 40.h), + ], ), ); } -} + + Widget _passListView(List passes) { + return ListView.builder( + itemCount: passes.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final pass = passes[index]; + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: InkWell( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.qrPage, + arguments: pass.id, // Pass your booking ID here + ); + }, + child: PassTicketCard(pass: pass), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/my_pass/views/pass_attraction_details_view.dart b/lib/my_pass/views/pass_attraction_details_view.dart index 032477e..5b4a432 100644 --- a/lib/my_pass/views/pass_attraction_details_view.dart +++ b/lib/my_pass/views/pass_attraction_details_view.dart @@ -2,6 +2,7 @@ import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet. 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/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -177,10 +178,142 @@ class PassAttractionDetailsView extends StatelessWidget { ], ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h), + child: Container( + width: double.infinity, + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(16.r), + border: Border.all( + color: Color(0xFFFDCDCE), + width: 1.5, + ), + ), + child: Column( + children: [ + Text( + "Scan this at the site of the attraction", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Color(0xFFF95F62), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 20.h), + // QR Code Image + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + ), + child: Image.asset( + 'assets/images/qr_image.png', + height: 200.h, + width: 200.w, + fit: BoxFit.contain, + ), + ), + SizedBox(height: 16.h), + // QR Code Text + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "IYFHHVN254ADSD", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + letterSpacing: 1.2, + ), + ), + SizedBox(width: 8.w), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: "IYFHHVN254ADSD")); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Code copied to clipboard'), + duration: Duration(seconds: 2), + backgroundColor: Color(0xFFF95F62), + ), + ); + }, + child: Icon( + Icons.copy, + size: 18.sp, + color: Colors.black54, + ), + ), + ], + ), + SizedBox(height: 20.h), + // Check in Button + SizedBox( + width: double.infinity, + height: 50.h, + child: ElevatedButton( + onPressed: () { + // Add your check-in logic here + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFFF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25.r), + ), + elevation: 0, + ), + child: Text( + "Check in", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + SizedBox(height: 12.h), + // Help Text + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Having problems redeeming the pass? ", + style: TextStyle( + fontSize: 12.sp, + color: Colors.black54, + ), + ), + GestureDetector( + onTap: () { + // Add your help/support navigation here + }, + child: Text( + "Click Here", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFFF95F62), + fontWeight: FontWeight.w600, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + ], + ), + ), + ), + // About Section Padding( padding: - EdgeInsets.only(left: 16.w, right: 16.w, top: 20.h), + EdgeInsets.only(left: 16.w, right: 16.w,), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/my_pass/views/pass_attractions_page_view.dart b/lib/my_pass/views/pass_attractions_page_view.dart index a4b427e..d350974 100644 --- a/lib/my_pass/views/pass_attractions_page_view.dart +++ b/lib/my_pass/views/pass_attractions_page_view.dart @@ -5,35 +5,43 @@ 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'; +import '../blocs/myPassesAttrctions/my_passes_attractions_bloc.dart'; +import '../blocs/myPassesAttrctions/my_passes_attractions_event.dart'; +import '../blocs/myPassesAttrctions/my_passes_attractions_state.dart'; +import '../repository/my_passes_attractions_repository.dart'; class PassAttractionsPage extends StatelessWidget { + final int cityXid; final String source; - const PassAttractionsPage({super.key, required this.source}); + + const PassAttractionsPage({ + super.key, + required this.cityXid, + required this.source, + }); @override Widget build(BuildContext context) { return BlocProvider( create: (_) { - final bloc = AttractionsBloc( - repository: AttractionsRepository(), + final bloc = MyPassesAttractionsBloc( + repository: MyPassesAttractionsRepository(), ); + // Fetch attractions with cityXid bloc.add( - const FetchAttractionsByCategory(), // No categoryXid parameter + FetchMyPassesAttractionsByCategory( + cityXid: cityXid, + ), ); return bloc; }, - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - final bloc = context.read(); + final bloc = context.read(); return Scaffold( backgroundColor: Colors.white, @@ -49,23 +57,22 @@ class PassAttractionsPage extends StatelessWidget { isProfilePage: false, showDivider: true, ), - backWidget(context, "Your Attraction", Colors.black), + backWidget(context, "Pass Attractions", Colors.black), const SizedBox(height: 20), - // 🔍 Search field (UI kept, logic disabled) + // 🔍 Search field with BLoC logic CommonSearchField( hint: "Search attractions...", hintColor: Colors.grey.shade500, onChanged: (value) { - // ❌ Search logic intentionally disabled - // UI only, no API call + bloc.add(SearchMyPassesAttractions(value)); }, ), const SizedBox(height: 16), - // 🏖️ Category chips row - DYNAMIC - if (state is AttractionsLoaded) + // 🖼️ Category chips row - DYNAMIC + if (state is MyPassesAttractionsLoaded) SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -73,10 +80,12 @@ class PassAttractionsPage extends StatelessWidget { .map( (category) => buildCategoryChip( category.categoryName ?? '', - isSelected: state.selectedCategoryId == category.id, + isSelected: + state.selectedCategoryId == category.id, onTap: () { bloc.add( - FetchAttractionsByCategory( + FetchMyPassesAttractionsByCategory( + cityXid: cityXid, categoryXid: category.id, ), ); @@ -86,54 +95,20 @@ class PassAttractionsPage extends StatelessWidget { .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) + // 🏙️ Attraction list with search filter + if (state is MyPassesAttractionsLoading) 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) + else if (state is MyPassesAttractionsLoaded) + _buildAttractionsList(state) + else if (state is MyPassesAttractionsError) Center( child: Padding( padding: const EdgeInsets.only(top: 60), @@ -157,4 +132,34 @@ class PassAttractionsPage extends StatelessWidget { ), ); } + + // Helper method to build attractions list + Widget _buildAttractionsList(MyPassesAttractionsLoaded state) { + if (state.filteredAttractions.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: Text( + state.searchQuery.isEmpty + ? "No attractions found" + : "No attractions match your search", + style: TextStyle( + color: Colors.grey, + fontSize: 14.sp, + ), + ), + ), + ); + } + + return Column( + children: state.filteredAttractions + .map( + (attraction) => PassAttractionCard( + attraction: attraction, + ), + ) + .toList(), + ); + } } \ 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 index e7574a0..f577566 100644 --- a/lib/my_pass/views/pass_details_page_view.dart +++ b/lib/my_pass/views/pass_details_page_view.dart @@ -1,4 +1,3 @@ -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'; @@ -8,16 +7,57 @@ 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'; +import '../../networkApiServices/api_urls.dart'; +import '../blocs/myPassesDetails/my_passes_details_bloc.dart'; +import '../blocs/myPassesDetails/my_passes_details_event.dart'; +import '../blocs/myPassesDetails/my_passes_details_state.dart'; -class PassDetailsView extends StatelessWidget { - const PassDetailsView({super.key}); +class PassDetailsView extends StatefulWidget { + final int bookingId; + + const PassDetailsView({super.key, required this.bookingId}); + + @override + State createState() => _PassDetailsViewState(); +} + +class _PassDetailsViewState extends State { + @override + void initState() { + super.initState(); + context.read().add( + FetchMyPassDetails(passId: widget.bookingId), + ); + } @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - if (state is MyPassLoaded) { - final pass = state.selectedPass!; + if (state is MyPassesDetailsLoading) { + return const Scaffold( + backgroundColor: Colors.white, + body: Center(child: CircularProgressIndicator()), + ); + } + + if (state is MyPassesDetailsError) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Text( + 'Error: ${state.message}', + style: GoogleFonts.poppins(color: Colors.red), + ), + ), + ); + } + + if (state is MyPassesDetailsLoaded) { + final data = state.data; + final city = data.city; + final attractions = data.attractions ?? []; + final offers = data.offers ?? []; return SafeArea( child: Scaffold( @@ -27,7 +67,6 @@ class PassDetailsView extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// App Bar SizedBox(height: 10.h), const CommonAppBar( @@ -44,144 +83,176 @@ class PassDetailsView extends StatelessWidget { /// ------------------------------- /// 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), + CustomPaint( + painter: DashedBorderPainter( + color: const Color(0xffF95F62), + radius: 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, - ), + 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( + '${(city?.cardMode ?? '').isNotEmpty + ? city!.cardMode![0].toUpperCase() + city.cardMode!.substring(1) + : ''} Card', + style: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: const Color(0xffF95F62), ), + ), + SizedBox(height: 18.h), + 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), + 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, + /// RIGHT CONTENT + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + /// Adults + Kids (WRAP prevents overflow) + Wrap( + spacing: 10.w, + runSpacing: 10.h, children: [ - Icon( - Icons.calendar_today_outlined, - size: 15.sp, - color: const Color(0xffF95F62), + _infoChip( + imagePath: "assets/icons/person.png", + text: "Adults-${city?.totalAdult ?? 0}", ), - 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), - ), + _infoChip( + imagePath: "assets/icons/person.png", + text: "Kids-${city?.totalChild ?? 0}", ), ], ), - ), - ], + + SizedBox(height: 12.h), + + /// Days Container (Full width) + _infoChip( + imagePath: "assets/icons/time.png", + text: "${city?.noOfDays ?? 0} Days", + isExpanded: true, + ), + + SizedBox(height: 14.h), + + /// Valid Till + Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + "assets/icons/calendar.png", + height: 15.h, + width: 15.w, + 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( + city?.validUpto ?? "", + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: const Color(0xffF95F62), + ), + ), + ], + ), + ), + ], + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), - ), - SizedBox(height: 24.h), + SizedBox(height: 24.h), _sectionTitle("Suggested Attractions"), SizedBox(height: 12.h), - _attractionCard(), - SizedBox(height: 12.h), - _attractionCard(), - + /// Display attractions from API + if (attractions.isNotEmpty) ...[ + ...attractions.take(2).map((attraction) => + Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.passAttractionDetails, + arguments: attraction.id, + ); + }, + child: _attractionCard( + title: attraction.title, + description: attraction.description, + image: attraction.image, + ticketPriceAdult: attraction.ticketPriceAdult, + ticketPriceChild: attraction.ticketPriceChild, + bookingEmail: attraction.bookingEmail, + bookingPhoneNumber: attraction.bookingPhoneNumber, + ), + ), + )), + ] else ...[ + _attractionCard( + title: 'No attractions available', + description: '', + image: '', + ticketPriceAdult: null, + ticketPriceChild: null, + bookingEmail: null, + bookingPhoneNumber: null, + ), + ], SizedBox(height: 16.h), - _outlineButton( "View all Attractions", () { Navigator.pushNamed( context, RouteConstants.passAttractionsPage, - arguments: "qrPass", + arguments: { + 'cityId': city?.id, + 'source': 'my_passes', + }, ); }, ), @@ -194,13 +265,64 @@ class PassDetailsView extends StatelessWidget { _sectionTitle("Recommended Offers"), SizedBox(height: 12.h), - Row( - children: [ - Expanded(child: _offerCard()), - SizedBox(width: 12.w), - Expanded(child: _offerCard()), - ], - ), + /// Display offers from API + if (offers.isNotEmpty) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.offerPassDetail, + arguments: offers[0].id, + ); + }, + child: _offerCard( + title: offers[0].title ?? '', + description: offers[0].description ?? '', + image: offers[0].mobileBannerImage != null + ? "${ApiUrls.baseUrl}/${offers[0].mobileBannerImage!}" + : '', + ), + ), + ), + + if (offers.length > 1) ...[ + SizedBox(width: 12.w), + Expanded( + child: GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.offerPassDetail, + arguments: offers[1].id, + ); + }, + child: _offerCard( + title: offers[1].title ?? '', + description: offers[1].description ?? '', + image: offers[1].mobileBannerImage != null + ? "${ApiUrls.baseUrl}/${offers[1].mobileBannerImage!}" + : '', + ), + ), + ), + ], + ], + ), + ] else ...[ + Row( + children: [ + Expanded( + child: _offerCard( + title: 'No offers available', + description: '', + image: '', + ), + ), + ], + ), + ], SizedBox(height: 16.h), @@ -210,6 +332,7 @@ class PassDetailsView extends StatelessWidget { Navigator.pushNamed( context, RouteConstants.searchPassOffer, + arguments: city?.id ??"", ); }, ), @@ -219,7 +342,7 @@ class PassDetailsView extends StatelessWidget { GestureDetector( onTap: () { Navigator.of(context).pushNamed( - RouteConstants.privacyPolicy, // ✅ pass offerId + RouteConstants.privacyPolicy, ); }, child: Center( @@ -227,6 +350,7 @@ class PassDetailsView extends StatelessWidget { "Learn about policies", style: GoogleFonts.poppins( fontSize: 12.sp, + fontWeight: FontWeight.w500, decoration: TextDecoration.underline, ), ), @@ -241,7 +365,10 @@ class PassDetailsView extends StatelessWidget { ); } - return const Center(child: CircularProgressIndicator()); + return const Scaffold( + backgroundColor: Colors.white, + body: Center(child: CircularProgressIndicator()), + ); }, ); } @@ -279,22 +406,53 @@ class PassDetailsView extends StatelessWidget { ); } - Widget _attractionCard() { + Widget _attractionCard({ + required String title, + required String description, + required String image, + num? ticketPriceAdult, + num? ticketPriceChild, + String? bookingEmail, + String? bookingPhoneNumber, + }) { + // Check if booking is required (both email and phone are empty/null) + final bool isBookingRequired = (bookingEmail == null || bookingEmail.isEmpty) && + (bookingPhoneNumber == null || bookingPhoneNumber.isEmpty); + + // Format the price display + String priceText = ticketPriceAdult != null + ? "from \$${ticketPriceAdult}/person" + : "Price not available"; + return Container( - padding: EdgeInsets.all(12.w), + padding: EdgeInsets.all(10.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, + child: image.isNotEmpty + ? Image.network( + image, + height: 100.w, + width: 90.w, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + "assets/images/aa4.png", + height: 100.w, + width: 90.w, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + "assets/images/aa4.png", + height: 100.w, width: 90.w, fit: BoxFit.cover, ), @@ -308,7 +466,7 @@ class PassDetailsView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Koh Rong Samloem", + title, style: GoogleFonts.poppins( fontWeight: FontWeight.w600, fontSize: 14.sp, @@ -318,17 +476,19 @@ class PassDetailsView extends StatelessWidget { SizedBox(height: 2.h), Text( - "Krong Siem Reap", + description, style: GoogleFonts.poppins( fontSize: 12.sp, color: Colors.grey.shade600, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), SizedBox(height: 4.h), Text( - "from \$25/person", + priceText, style: GoogleFonts.poppins( fontSize: 12.sp, fontWeight: FontWeight.w500, @@ -337,23 +497,25 @@ class PassDetailsView extends StatelessWidget { 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, + // Show "Booking Required" tag only if both email and phone are null/empty + if (isBookingRequired) + 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, + ), ), ), - ) ], ), ), @@ -381,20 +543,31 @@ class PassDetailsView extends StatelessWidget { ); } + Widget _infoChip({ - required IconData icon, + required String imagePath, // 👈 image asset path required String text, + bool isExpanded = false, }) { return Container( + width: isExpanded ? double.infinity : null, 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, + mainAxisSize: + isExpanded ? MainAxisSize.max : MainAxisSize.min, + mainAxisAlignment: + isExpanded ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ - Icon(icon, size: 14.sp, color: const Color(0xffF95F62)), + Image.asset( + imagePath, + height: 14.h, + width: 14.w, + color: const Color(0xffF95F62), // remove if your icon has its own color + ), SizedBox(width: 6.w), Text( text, @@ -409,7 +582,11 @@ class PassDetailsView extends StatelessWidget { ); } - Widget _offerCard() { + Widget _offerCard({ + required String title, + required String description, + required String image, + }) { return Container( padding: EdgeInsets.all(6.w), decoration: BoxDecoration( @@ -419,13 +596,27 @@ class PassDetailsView extends StatelessWidget { 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 + child: image.isNotEmpty + ? Image.network( + image, + height: 120.h, + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + "assets/images/aa4.png", + height: 120.h, + width: double.infinity, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + "assets/images/aa4.png", + height: 120.h, width: double.infinity, fit: BoxFit.cover, ), @@ -435,26 +626,30 @@ class PassDetailsView extends StatelessWidget { /// 🔥 Title Text( - "Astor Hotels Ultra Deluxe", + title, style: GoogleFonts.poppins( fontWeight: FontWeight.w600, fontSize: 16.sp, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), SizedBox(height: 6.h), /// 🔥 Description Text( - "15% Discount on all treatments for first-time clients", + description, style: GoogleFonts.poppins( fontSize: 12.sp, color: Colors.grey.shade700, height: 1.4, ), + maxLines: 3, + overflow: TextOverflow.ellipsis, ), ], ), ); } -} +} \ No newline at end of file diff --git a/lib/my_pass/views/search_pass_offers_with_listing.dart b/lib/my_pass/views/search_pass_offers_with_listing.dart index 37cedd9..cacec05 100644 --- a/lib/my_pass/views/search_pass_offers_with_listing.dart +++ b/lib/my_pass/views/search_pass_offers_with_listing.dart @@ -2,19 +2,25 @@ 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/services.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'; +import '../blocs/myPassesOffers/my_passes_offers_bloc.dart'; +import '../blocs/myPassesOffers/my_passes_offers_event.dart'; +import '../blocs/myPassesOffers/my_passes_offers_state.dart'; +import '../repository/my_passes_offers_repository.dart'; class PassOffersScreen extends StatefulWidget { - const PassOffersScreen({super.key}); + final int cityId; + + const PassOffersScreen({ + super.key, + required this.cityId, + }); @override State createState() => _PassOffersScreenState(); @@ -26,7 +32,8 @@ class _PassOffersScreenState extends State { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => OffersBloc(OffersRepository())..add(LoadOffers()), + create: (_) => MyPassesOffersBloc(MyPassesOffersRepository()) + ..add(LoadMyPassesOffers(cityXid: widget.cityId)), child: Scaffold( backgroundColor: Colors.white, body: SafeArea( @@ -62,82 +69,88 @@ class _PassOffersScreenState extends State { hintColor: const Color(0xFFF95F62).withOpacity(.6), showSuffix: true, onChanged: (value) { - context.read().add(SearchOffers(value)); + context.read().add(SearchMyPassesOffers(value)); }, ), ), SizedBox(height: 20.h), /// Dynamic Categories - BlocBuilder( + BlocBuilder( builder: (context, state) { - if (state is OffersLoaded) { + if (state is MyPassesOffersLoaded) { 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 Align( + alignment: Alignment.centerLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + 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( + 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(LoadMyPassesOffers(cityXid: widget.cityId)); + } else { + // Select new category + selectedCategoryId = category.id; + context.read().add( + LoadMyPassesOffers( + cityXid: widget.cityId, + categoryXid: category.id, + ), + ); + } + }); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, + ), + decoration: BoxDecoration( color: isSelected ? Color(0xFFF95F62) - : Color(0xFFFDCDCE), + : 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), + child: Center( + child: CustomText( + text: category.categoryName, + color: isSelected + ? Colors.white + : Color(0xFFF95F62), + ), ), ), ), - ), - ); - }), - ], + ); + }), + ], + ), ), ); } @@ -149,9 +162,9 @@ class _PassOffersScreenState extends State { /// Offer list Expanded( - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - if (state is OffersLoading) { + if (state is MyPassesOffersLoading) { return const Center( child: CircularProgressIndicator( color: Color(0xFFF95F62), @@ -159,7 +172,7 @@ class _PassOffersScreenState extends State { ); } - if (state is OffersError) { + if (state is MyPassesOffersError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -171,7 +184,7 @@ class _PassOffersScreenState extends State { ), SizedBox(height: 16.h), Text( - "Error: ${state.message}", + state.message, style: TextStyle( color: Colors.red, fontSize: 14.sp, @@ -183,7 +196,7 @@ class _PassOffersScreenState extends State { ); } - if (state is OffersLoaded) { + if (state is MyPassesOffersLoaded) { final offers = state.offers; if (offers.isEmpty) { @@ -240,6 +253,7 @@ class _PassOffersScreenState extends State { borderRadius: BorderRadius.circular(12.sp), ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: @@ -310,17 +324,64 @@ class _PassOffersScreenState extends State { CustomText( text: offer.title, size: 18.sp, - maxLines: 2, + maxLines: 1, overflow: TextOverflow.ellipsis, ), SizedBox(height: 8.h), - CustomText( - text: offer.description, - color: Colors.black.withOpacity(.6), - size: 12.sp, - maxLines: 3, - overflow: TextOverflow.ellipsis, + Expanded( + child: CustomText( + text: offer.description, + color: Colors.black.withOpacity(.6), + size: 12.sp, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), ), + if (offer.offerCode != null && offer.offerCode!.isNotEmpty) ...[ + SizedBox(height: 8.h), + GestureDetector( + onTap: () { + Clipboard.setData( + ClipboardData(text: offer.offerCode!), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Code copied: ${offer.offerCode!}"), + duration: Duration(seconds: 1), + backgroundColor: Color(0xFFF95F62), + ), + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 6.h, + ), + decoration: BoxDecoration( + color: Color(0xFFFEE7E7), + borderRadius: BorderRadius.circular(6.sp), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CustomText( + text: offer.offerCode!, + size: 12.sp, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Icon( + Icons.copy, + size: 16.sp, + color: Color(0xFFF95F62), + ), + ], + ), + ), + ), + ], ], ), ), diff --git a/lib/my_pass/widgets/pass_attraction_card.dart b/lib/my_pass/widgets/pass_attraction_card.dart index 6519c40..60972bb 100644 --- a/lib/my_pass/widgets/pass_attraction_card.dart +++ b/lib/my_pass/widgets/pass_attraction_card.dart @@ -20,6 +20,16 @@ class PassAttractionCard extends StatelessWidget { /// GALLERY IMAGE (handled safely in model) final String imageUrl = attraction.coverImageUrl; + /// Show "Booking Required" when both email and phone are empty/null + final bool showBookingRequired = + (attraction.bookingEmail.isEmpty || attraction.bookingEmail == null) || + (attraction.bookingPhoneNumber.isEmpty || attraction.bookingPhoneNumber == null); + + /// Format the price display + String priceText = attraction.ticketPriceAdult != null + ? "from \$${attraction.ticketPriceAdult}/person" + : "Price not available"; + return InkWell( onTap: () { Navigator.of(context).pushNamed( @@ -29,85 +39,94 @@ class PassAttractionCard extends StatelessWidget { }, child: Container( margin: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.w), - padding: EdgeInsets.all(12.w), + padding: EdgeInsets.all(10.w), decoration: BoxDecoration( - border: Border.all(color: const Color(0xffFDCDCE)), - borderRadius: BorderRadius.circular(15.r), - color: const Color(0xffFFF5F5), + borderRadius: BorderRadius.circular(16.r), + border: Border.all(color: const Color(0xffF2D6D6)), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// IMAGE (network with fallback) + /// 🔥 Attraction Image (Real Image Style Box) ClipRRect( - borderRadius: BorderRadius.circular(8.r), + borderRadius: BorderRadius.circular(12.r), child: imageUrl.isNotEmpty ? Image.network( imageUrl, - height: 94.h, - width: 94.w, + height: 100.w, + width: 90.w, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => _imageFallback(), + errorBuilder: (context, error, stackTrace) { + return _imageFallback(); + }, ) : _imageFallback(), ), - SizedBox(width: 10.w), + SizedBox(width: 12.w), - /// CONTENT + /// 🔥 Text Section Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( attraction.title, - style: TextStyle( - fontSize: 16.sp, + style: GoogleFonts.poppins( + fontWeight: FontWeight.w600, + fontSize: 14.sp, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + + SizedBox(height: 2.h), + + Text( + attraction.description, + style: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.grey.shade600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + SizedBox(height: 4.h), + + Text( + priceText, + style: GoogleFonts.poppins( + fontSize: 12.sp, fontWeight: FontWeight.w500, ), ), SizedBox(height: 6.h), - Text( - attraction.address, - style: GoogleFonts.poppins( - fontSize: 12.sp, - fontWeight: FontWeight.w400, - color: const Color(0xff464646), + /// TAGS (CARD TITLES) OR BOOKING REQUIRED + showBookingRequired + ? Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.h, ), - ), - - 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, - ), - ), - ], + decoration: BoxDecoration( + color: const Color(0xffC1D2F8), + border: Border.all( + color: const Color(0xff2563EB), + ), + borderRadius: BorderRadius.circular(20.r), ), - ), - - SizedBox(height: 6.h), - - /// TAGS (CARD TITLES) - attraction.isBookingRequired == false - ? Wrap( + child: Text( + "Booking Required", + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: const Color(0xff1A1A1A), + fontWeight: FontWeight.w400, + ), + ), + ) + : Wrap( spacing: 6.w, runSpacing: 6.h, children: tags @@ -130,8 +149,7 @@ class PassAttractionCard extends StatelessWidget { ? const Color(0xffF95FAF) : const Color(0xffF95F62), ), - borderRadius: - BorderRadius.circular(20.r), + borderRadius: BorderRadius.circular(20.r), ), child: Text( tag, @@ -144,48 +162,42 @@ class PassAttractionCard extends StatelessWidget { ), ) .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, - ), - ), ), ], ), ), + + SizedBox(width: 8.w), + + /// 🔥 QR Code Circle (Proper UI like Design) + Container( + height: 44.w, + width: 44.w, + decoration: const BoxDecoration( + color: 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, + ), + ), + ), ], ), ), ); } - /// SAME PLACEHOLDER AS BEFORE + /// Image Fallback Widget 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, - ), + return Image.asset( + "assets/images/aa4.png", + height: 100.w, + width: 90.w, + fit: BoxFit.cover, ); } -} +} \ No newline at end of file diff --git a/lib/my_pass/widgets/pass_widget.dart b/lib/my_pass/widgets/pass_widget.dart index ace5562..6f864a5 100644 --- a/lib/my_pass/widgets/pass_widget.dart +++ b/lib/my_pass/widgets/pass_widget.dart @@ -1,24 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../models/my_passes_model.dart'; class PassTicketCard extends StatelessWidget { - final dynamic pass; + final MyPassData pass; const PassTicketCard({super.key, required this.pass}); @override Widget build(BuildContext context) { - // Dimensions tuned to your screenshot final double cardWidth = MediaQuery.of(context).size.width - 32.w; - final double topSectionHeight = 105.h; // where dotted line sits + final double topSectionHeight = 105.h; final double bottomSectionHeight = 50.h; final double cardHeight = topSectionHeight + bottomSectionHeight; return SizedBox( width: cardWidth, child: CustomPaint( - // paints white background, border, corner radius, side cuts, shadow, and divider dots painter: _TicketBackgroundPainter( cornerRadius: 16.r, notchRadius: 9.r, @@ -27,7 +26,6 @@ class PassTicketCard extends StatelessWidget { shadowColor: Colors.black.withOpacity(0.08), ), child: ClipPath( - // actual clipping so child content never bleeds outside the shape clipper: _TicketClipper( cornerRadius: 16.r, notchRadius: 9.r, @@ -37,32 +35,36 @@ class PassTicketCard extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), child: Column( children: [ - // ---------- TOP SECTION ---------- SizedBox( - height: topSectionHeight - 12.h, // keep space for the dots line + height: topSectionHeight - 12.h, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // thumbnail ClipRRect( borderRadius: BorderRadius.circular(10.r), - child: Image.asset( - pass.imageUrl, + child: Image.network( + pass.city?.bannerImage ?? '', height: 80.h, width: 80.w, fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 80.h, + width: 80.w, + color: Colors.grey[300], + child: Icon(Icons.image, size: 40), + ); + }, ), ), SizedBox(width: 10.w), - - // details Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - if (pass.isActive) + if (pass.bookingStatus == "active") Container( padding: EdgeInsets.symmetric( horizontal: 8.w, vertical: 3.h), @@ -81,7 +83,7 @@ class PassTicketCard extends StatelessWidget { ), SizedBox(width: 8.w), Text( - pass.duration, // "2 Days" + "${pass.noOfDays ?? 0} Days", style: GoogleFonts.poppins( color: Colors.black87, fontSize: 12.sp, @@ -91,7 +93,9 @@ class PassTicketCard extends StatelessWidget { ), SizedBox(height: 10.h), Text( - pass.title, + "${(pass.cardMode?.isNotEmpty ?? false) + ? pass.cardMode![0].toUpperCase() + pass.cardMode!.substring(1) + : ''} Card", style: GoogleFonts.poppins( fontWeight: FontWeight.w600, fontSize: 18.sp, @@ -100,7 +104,7 @@ class PassTicketCard extends StatelessWidget { ), SizedBox(height: 4.h), Text( - "Adults-${pass.adults} • Kids-${pass.kids}", + "Adults-${pass.totalAdult ?? 0} • Kids-${pass.totalChild ?? 0}", style: GoogleFonts.poppins( color: Colors.black54, fontSize: 11.sp, @@ -109,8 +113,6 @@ class PassTicketCard extends StatelessWidget { ], ), ), - - // QR chip CircleAvatar( radius: 20.r, backgroundColor: Color(0xffFEE7E7), @@ -122,26 +124,21 @@ class PassTicketCard extends StatelessWidget { ], ), ), - - // space exactly where the dotted line is painted by the painter SizedBox(height: 15.h), - - // ---------- BOTTOM SECTION ---------- Padding( padding: EdgeInsets.symmetric(horizontal: 4.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Valid Till: ${pass.validity}", + "Valid Till: ${pass.validUpto ?? ''}", style: GoogleFonts.poppins( - fontSize: 11.sp, - color: Colors.black, - fontWeight: FontWeight.w400 - ), + fontSize: 11.sp, + color: Colors.black, + fontWeight: FontWeight.w400), ), Text( - pass.city, // "Melbourne" + pass.city?.name ?? '', style: GoogleFonts.poppins( fontWeight: FontWeight.w500, fontSize: 13.sp, @@ -159,7 +156,6 @@ class PassTicketCard extends StatelessWidget { } } -/// Clips the ticket with rounded corners and 2 side “cuts” centered at dividerY class _TicketClipper extends CustomClipper { final double cornerRadius; final double notchRadius; @@ -180,10 +176,11 @@ class _TicketClipper extends CustomClipper { )); final cuts = Path() - ..addOval(Rect.fromCircle(center: Offset(0, dividerY), radius: notchRadius)) - ..addOval(Rect.fromCircle(center: Offset(size.width, dividerY), radius: notchRadius)); + ..addOval(Rect.fromCircle( + center: Offset(0, dividerY), radius: notchRadius)) + ..addOval(Rect.fromCircle( + center: Offset(size.width, dividerY), radius: notchRadius)); - // Rounded-rect MINUS the two circles return Path.combine(PathOperation.difference, rrectPath, cuts); } @@ -194,8 +191,6 @@ class _TicketClipper extends CustomClipper { dividerY != old.dividerY; } - -/// Paints fill, border, shadow and the dotted perforation line class _TicketBackgroundPainter extends CustomPainter { final double cornerRadius; final double notchRadius; @@ -224,35 +219,30 @@ class _TicketBackgroundPainter extends CustomPainter { void paint(Canvas canvas, Size size) { final path = _ticketPath(size); - // Realistic layered shadow canvas.save(); - canvas.translate(0, 2); // tiny downward offset for depth + canvas.translate(0, 2); final shadowPaint = Paint() ..color = Colors.black.withOpacity(0.10) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6); canvas.drawPath(path, shadowPaint); canvas.restore(); - // Subtle ambient shadow (light spread around) final ambientShadowPaint = Paint() ..color = Colors.black.withOpacity(0.04) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12); canvas.drawPath(path, ambientShadowPaint); - // Fill background final fillPaint = Paint() ..style = PaintingStyle.fill ..color = const Color(0xffFFFBFB); canvas.drawPath(path, fillPaint); - // Border stroke final strokePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 0.8 ..color = const Color(0xffE5E5E5); canvas.drawPath(path, strokePaint); - // 🔹 Dotted perforation line final dashPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1 @@ -282,4 +272,4 @@ class _TicketBackgroundPainter extends CustomPainter { borderColor != oldDelegate.borderColor || shadowColor != oldDelegate.shadowColor; } -} +} \ No newline at end of file diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 25c2b00..9d96d80 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -1,7 +1,7 @@ class ApiUrls { - static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API - // static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API + // static const baseUrl = "https://devapi.citycards.betadelivery.com";//Normal API + static const baseUrl = "https://testingapi.citycards.betadelivery.com";// Test API // static const baseUrl = "https://uatapi.citycard.betadelivery.com";// Production Lvl API static const refreshToken = "$baseUrl/auth/refresh"; @@ -10,15 +10,20 @@ class ApiUrls { // static const upcomingCityList = "$baseUrl/mobile/upcoming_cities"; static const searchCityList = "$baseUrl/mobile/city-selection"; static const attractionsList = "$baseUrl/mobile/list/all"; + static const passAttractionsList = "$baseUrl/mobile/passes/mobile/list"; static const attractionDetails = "$baseUrl/mobile/list"; static const home = "$baseUrl/mobile"; static const faqPrivacyTerms = "$baseUrl/mobile/user/cms-data"; static const userProfile = "$baseUrl/mobile/user"; static const offers = "$baseUrl/mobile/list/offers"; + static const passOffers = "$baseUrl/mobile/passes/mobile/list/offers"; static const buyAPass = "$baseUrl/mobile/pass"; static const offersDetails = "$baseUrl/mobile/list/offers"; static const myPostCards = "$baseUrl/mobile/postcards/all"; static const coupons = "$baseUrl/mobile/passes/dropdown/card"; + static const myPasses = "$baseUrl/mobile/passes/all"; + static const passDetails = "$baseUrl/mobile/passes"; + static const myPassesCart = "$baseUrl/mobile/passes/cart/passes"; static const myItineraries = "$baseUrl/mobile/itinerary/all-initineraries"; static const getItineraryCities = "$baseUrl/mobile/itinerary/cities-with-icons"; diff --git a/lib/offer_pass_detail/offer_pass_detail_view.dart b/lib/offer_pass_detail/offer_pass_detail_view.dart index f4572ff..1638d1e 100644 --- a/lib/offer_pass_detail/offer_pass_detail_view.dart +++ b/lib/offer_pass_detail/offer_pass_detail_view.dart @@ -24,7 +24,7 @@ class OffersDetailsView extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (_) => OfferDetailsBloc( - repository: OffersDetailsRepository(), // ← Create directly + repository: OffersDetailsRepository(), // ✅ Create directly )..add(FetchOfferDetailsEvent(offerId: offerId)), child: const _OffersDetailsContent(), ); @@ -106,12 +106,16 @@ class _OffersDetailsContent extends StatelessWidget { ), ), SizedBox(width: 8.w), - Text( - offer.partnerName, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: Colors.white, + Expanded( + child: Text( + offer.partnerName, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], @@ -125,6 +129,7 @@ class _OffersDetailsContent extends StatelessWidget { Positioned( bottom: 31.h, left: 12.w, + right: 60.w, child: Text( offer.partnerName, style: TextStyle( @@ -133,6 +138,8 @@ class _OffersDetailsContent extends StatelessWidget { fontWeight: FontWeight.w500, height: 1.2, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), @@ -299,4 +306,4 @@ class _OffersDetailsContent extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart new file mode 100644 index 0000000..2ddfa2d --- /dev/null +++ b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart @@ -0,0 +1,63 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/postcard_add_to_cart_repository.dart'; +import 'add_to_cart_postcard_event.dart'; +import 'add_to_cart_postcard_state.dart'; + +class AddToCartPostCardBloc + extends Bloc { + final AddToCartPostCardRepository repository; + + AddToCartPostCardBloc(this.repository) + : super(AddToCartPostCardInitial()) { + on(_onAddToCartRequested); + } + + Future _onAddToCartRequested( + AddToCartPostCardRequested event, + Emitter emit, + ) async { + try { + emit(AddToCartPostCardLoading()); + + final response = await repository.addToCartPostCard( + countryName: event.countryName, + cityName: event.cityName, + stateName: event.stateName, + zipCode: event.zipCode, + address1: event.address1, + address2: event.address2, + pcTitle: event.pcTitle, + pcContent: event.pcContent, + pcImageFile: event.pcImageFile, + pcNumber: event.pcNumber, + pcDatetime: event.pcDatetime, + fullname: event.fullname, + emailAddress: event.emailAddress, + mobileNumber: event.mobileNumber, + isdCode: event.isdCode, + isForSelf: true, // API default + isDraft: true, // API default + baseAmount: 0, + totalTaxAmount: 0, + totalAmount: 0, + ); + + final postcard = response['postcard']; + + emit( + AddToCartPostCardSuccess( + postcardId: postcard['id'], + pcNumber: postcard['pcNumber'], + baseAmount: (postcard['baseAmount'] as num).toDouble(), + totalTaxAmount: (postcard['totalTaxAmount'] as num).toDouble(), + totalAmount: (postcard['totalAmount'] as num).toDouble(), + pcDatetime: postcard['pcDatetime'], + ), + ); + } catch (e) { + log('❌ AddToCartPostCardBloc Error', error: e); + emit(AddToCartPostCardFailure(e.toString())); + } + } +} diff --git a/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart new file mode 100644 index 0000000..5aece8e --- /dev/null +++ b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_event.dart @@ -0,0 +1,64 @@ +import 'dart:io'; +import 'package:equatable/equatable.dart'; + +abstract class AddToCartPostCardEvent extends Equatable { + const AddToCartPostCardEvent(); + + @override + List get props => []; +} + +class AddToCartPostCardRequested extends AddToCartPostCardEvent { + final String countryName; + final String cityName; + final String stateName; + final String zipCode; + final String? address1; + final String? address2; + final String pcTitle; + final String pcContent; + final File pcImageFile; + final String pcNumber; + final String pcDatetime; + final String fullname; + final String emailAddress; + final String mobileNumber; + final String isdCode; + + AddToCartPostCardRequested({ + required this.countryName, + required this.cityName, + required this.stateName, + required this.zipCode, + this.address1, + this.address2, + required this.pcTitle, + required this.pcContent, + required this.pcImageFile, + required this.pcNumber, + required this.pcDatetime, + required this.fullname, + required this.emailAddress, + required this.mobileNumber, + required this.isdCode, + }); + + @override + List get props => [ + countryName, + cityName, + stateName, + zipCode, + address1, + address2, + pcTitle, + pcContent, + pcImageFile, + pcNumber, + pcDatetime, + fullname, + emailAddress, + mobileNumber, + isdCode, + ]; +} diff --git a/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_state.dart b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_state.dart new file mode 100644 index 0000000..7af90c7 --- /dev/null +++ b/lib/postcard/blocs/addToCartPostcard/add_to_cart_postcard_state.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; + +abstract class AddToCartPostCardState extends Equatable { + const AddToCartPostCardState(); + + @override + List get props => []; +} + +class AddToCartPostCardInitial extends AddToCartPostCardState {} + +class AddToCartPostCardLoading extends AddToCartPostCardState {} + +class AddToCartPostCardSuccess extends AddToCartPostCardState { + final int postcardId; + final String pcNumber; + final double baseAmount; + final double totalTaxAmount; + final double totalAmount; + final String pcDatetime; + + const AddToCartPostCardSuccess({ + required this.postcardId, + required this.pcNumber, + required this.baseAmount, + required this.totalTaxAmount, + required this.totalAmount, + required this.pcDatetime, + }); + + @override + List get props => [ + postcardId, + pcNumber, + baseAmount, + totalTaxAmount, + totalAmount, + ]; +} + +class AddToCartPostCardFailure extends AddToCartPostCardState { + final String message; + + const AddToCartPostCardFailure(this.message); + + @override + List get props => [message]; +} diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart index 016b511..41b8af1 100644 --- a/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart +++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart @@ -60,6 +60,7 @@ class PostcardCheckoutBloc baseAmount: event.baseAmount, totalTaxAmount: event.totalTaxAmount, totalAmount: event.totalAmount, + postcardId: event.postcardId, )); } @@ -68,37 +69,19 @@ class PostcardCheckoutBloc emit(state.copyWith(isLoading: true, error: null, isSuccess: false)); try { - // Validate that image file exists before submitting - if (state.pcImageFile == null) { + // ⭐ Validate pcId exists + if (state.postcardId == null) { emit(state.copyWith( isLoading: false, - error: 'Please select a postcard image', + error: 'Postcard ID is missing', isSuccess: false, )); return; } final response = await repository.createPostCard( - countryName: state.countryName, - cityName: state.cityName, - stateName: state.stateName, - zipCode: state.zipCode, - address1: state.address1.isNotEmpty ? state.address1 : null, - address2: state.address2.isNotEmpty ? state.address2 : null, - pcTitle: state.pcTitle, - pcContent: state.pcContent, - pcImageFile: state.pcImageFile!, - pcNumber: state.pcNumber, - pcDatetime: state.pcDatetime, - fullname: state.fullname, - emailAddress: state.emailAddress, - mobileNumber: state.mobileNumber, - isdCode: state.isdCode, - isForSelf: state.isForSelf, - isDraft: true, // Save as draft - baseAmount: state.baseAmount, - totalTaxAmount: state.totalTaxAmount, - totalAmount: state.totalAmount, + pcId: state.postcardId!, + isDraft: true, // ⭐ Save as draft ); // Extract order ID from response if available @@ -126,67 +109,44 @@ class PostcardCheckoutBloc emit(state.copyWith(isLoading: true, error: null, isSuccess: false)); try { - // Validate that image file exists before submitting - if (state.pcImageFile == null) { + // ⭐ Validate pcId exists + if (state.postcardId == null) { emit(state.copyWith( isLoading: false, - error: 'Please select a postcard image', + error: 'Postcard ID is missing', isSuccess: false, )); return; } final response = await repository.createPostCard( - countryName: state.countryName, - cityName: state.cityName, - stateName: state.stateName, - zipCode: state.zipCode, - address1: state.address1.isNotEmpty ? state.address1 : null, - address2: state.address2.isNotEmpty ? state.address2 : null, - pcTitle: state.pcTitle, - pcContent: state.pcContent, - pcImageFile: state.pcImageFile!, - pcNumber: state.pcNumber, - pcDatetime: state.pcDatetime, - fullname: state.fullname, - emailAddress: state.emailAddress, - mobileNumber: state.mobileNumber, - isdCode: state.isdCode, - isForSelf: state.isForSelf, - isDraft: false, // Final submission (payment) - baseAmount: state.baseAmount, - totalTaxAmount: state.totalTaxAmount, - totalAmount: state.totalAmount, + pcId: state.postcardId!, + isDraft: false, // ⭐ Initiate payment ); - // 🆕 Parse response from backend - // Expected: {"postcardId": 16, "clientSecret": "pi_3Sx0yjRtCkWyT4Em1MKw1FeU_secret_S8M74wnEhTRC9lUz9RqJnuuqg"} - final postcardId = response['postcardId'] as int?; final clientSecret = response['clientSecret'] as String?; + final status = response['status'] as String?; - // Also try alternative key names in case backend uses different naming final orderId = response['orderId']?.toString() ?? response['order_id']?.toString() ?? response['id']?.toString(); - // Validate clientSecret is present if (clientSecret == null || clientSecret.isEmpty) { emit(state.copyWith( isLoading: false, - error: 'Payment initialization failed - no client secret received from server', + error: 'Payment initialization failed - no client secret received', isSuccess: false, )); return; } - // 🆕 Emit success with clientSecret for payment processing emit(state.copyWith( isLoading: false, isSuccess: true, isDraft: false, - postcardId: postcardId, - clientSecret: clientSecret, // This will trigger payment flow + postcardId: postcardId ?? state.postcardId, + clientSecret: clientSecret, orderId: orderId, )); } catch (e) { diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart index 765e6a7..db234da 100644 --- a/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart +++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart @@ -44,7 +44,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent { final String? address2; final String? pcTitle; final String? pcContent; - final File? pcImageFile; // ⭐ CHANGED: File instead of String + final File? pcImageFile; final String? pcNumber; final String? pcDatetime; final String? fullname; @@ -55,6 +55,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent { final double? baseAmount; final double? totalTaxAmount; final double? totalAmount; + final int? postcardId; // ⭐ ADD THIS UpdateCheckoutDataEvent({ this.countryName, @@ -65,7 +66,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent { this.address2, this.pcTitle, this.pcContent, - this.pcImageFile, // ⭐ CHANGED + this.pcImageFile, this.pcNumber, this.pcDatetime, this.fullname, @@ -76,6 +77,7 @@ class UpdateCheckoutDataEvent extends PostcardCheckoutEvent { this.baseAmount, this.totalTaxAmount, this.totalAmount, + this.postcardId, // ⭐ ADD THIS }); } diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index d7598f9..4ca042b 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -247,6 +247,19 @@ class PostcardCreationBloc on((event, emit) { emit(state.copyWith(isGift: event.isGift)); }); + + on((event, emit) { + emit(state.copyWith( + userProfileFullName: event.fullName, + userProfileEmail: event.email, + userProfilePhone: event.phone, + userProfileAddress: event.address, + userProfileCity: event.city, + userProfileState: event.state, + userProfileZipCode: event.zipCode, + userProfileCountry: event.country, + )); + }); } // Add this getter method in PostcardCreationBloc class diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart index 30fa6d2..7168ede 100644 --- a/lib/postcard/blocs/postcard_creation_events.dart +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -68,4 +68,27 @@ class UpdatePostcardNumber extends PostcardCreationEvent { final String pcNumber; UpdatePostcardNumber(this.pcNumber); +} + +// Event to store user profile data when "Buy for Myself" is selected +class StoreUserProfileData extends PostcardCreationEvent { + final String? fullName; + final String? email; + final String? phone; + final String? address; + final String? city; + final String? state; + final String? zipCode; + final String? country; + + StoreUserProfileData({ + this.fullName, + this.email, + this.phone, + this.address, + this.city, + this.state, + this.zipCode, + this.country, + }); } \ No newline at end of file diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart index 3024bf7..06ba104 100644 --- a/lib/postcard/blocs/postcard_creation_state.dart +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -20,7 +20,17 @@ class PostcardCreationState { final String? country; final String? state; final String? zipCode; - final String? pcNumber; // 🆕 ADD THIS + final String? pcNumber; + + // User's profile data (for "Buy for Myself" option) + final String? userProfileFullName; + final String? userProfileEmail; + final String? userProfilePhone; + final String? userProfileAddress; + final String? userProfileCity; + final String? userProfileState; + final String? userProfileZipCode; + final String? userProfileCountry; const PostcardCreationState({ required this.currentStep, @@ -41,7 +51,16 @@ class PostcardCreationState { this.state, this.zipCode, this.pcNumber, - required this.address, // 🆕 ADD THIS + required this.address, + // User profile data + this.userProfileFullName, + this.userProfileEmail, + this.userProfilePhone, + this.userProfileAddress, + this.userProfileCity, + this.userProfileState, + this.userProfileZipCode, + this.userProfileCountry, }); PostcardCreationState copyWith({ @@ -63,7 +82,16 @@ class PostcardCreationState { String? country, String? state, String? zipCode, - String? pcNumber, // 🆕 ADD THIS + String? pcNumber, + // User profile fields + String? userProfileFullName, + String? userProfileEmail, + String? userProfilePhone, + String? userProfileAddress, + String? userProfileCity, + String? userProfileState, + String? userProfileZipCode, + String? userProfileCountry, }) { return PostcardCreationState( currentStep: currentStep ?? this.currentStep, @@ -84,7 +112,16 @@ class PostcardCreationState { country: country ?? this.country, state: state ?? this.state, zipCode: zipCode ?? this.zipCode, - pcNumber: pcNumber ?? this.pcNumber, // 🆕 ADD THIS + pcNumber: pcNumber ?? this.pcNumber, + // User profile data + userProfileFullName: userProfileFullName ?? this.userProfileFullName, + userProfileEmail: userProfileEmail ?? this.userProfileEmail, + userProfilePhone: userProfilePhone ?? this.userProfilePhone, + userProfileAddress: userProfileAddress ?? this.userProfileAddress, + userProfileCity: userProfileCity ?? this.userProfileCity, + userProfileState: userProfileState ?? this.userProfileState, + userProfileZipCode: userProfileZipCode ?? this.userProfileZipCode, + userProfileCountry: userProfileCountry ?? this.userProfileCountry, ); } } \ No newline at end of file diff --git a/lib/postcard/repository/postcard_add_to_cart_repository.dart b/lib/postcard/repository/postcard_add_to_cart_repository.dart new file mode 100644 index 0000000..7ff00ad --- /dev/null +++ b/lib/postcard/repository/postcard_add_to_cart_repository.dart @@ -0,0 +1,203 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +import '../../networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class AddToCartPostCardRepository { + final NetworkApiService _apiServices = NetworkApiService(); + + /// Create / Save Postcard (Draft or Final) + /// ⭐ UPDATED: Now uses multipart/form-data for file upload + Future> addToCartPostCard({ + required String countryName, + required String cityName, + required String stateName, + required String zipCode, + + String? address1, // NOT required + String? address2, // NOT required + + required String pcTitle, + required String pcContent, + required File pcImageFile, // ⭐ CHANGED: File instead of String + required String pcNumber, + required String pcDatetime, + + required String fullname, + required String emailAddress, + required String mobileNumber, + required String isdCode, + + required bool isForSelf, + required bool isDraft, + + required double baseAmount, + required double totalTaxAmount, + required double totalAmount, + }) async { + try { + log('🟡 createPostCard() called'); + + if (kDebugMode) { + print('📤 [CREATE POSTCARD] Country: $countryName'); + print('📤 [CREATE POSTCARD] City: $cityName'); + print('📤 [CREATE POSTCARD] State: $stateName'); + print('📤 [CREATE POSTCARD] Zip: $zipCode'); + print('📤 [CREATE POSTCARD] Title: $pcTitle'); + print('📤 [CREATE POSTCARD] Number: $pcNumber'); + print('📤 [CREATE POSTCARD] Image File: ${pcImageFile.path}'); + print('📤 [CREATE POSTCARD] Is Draft: $isDraft'); + } + + // ⭐ Create FormData for multipart/form-data upload + final formData = FormData(); + + // Add text fields + formData.fields.addAll([ + MapEntry('countryName', countryName), + MapEntry('cityName', cityName), + MapEntry('stateName', stateName), + MapEntry('zipCode', zipCode), + MapEntry('pcTitle', pcTitle), + MapEntry('pcContent', pcContent), + MapEntry('pcNumber', pcNumber), + MapEntry('pcDatetime', pcDatetime), + MapEntry('fullname', fullname), + MapEntry('emailAddress', emailAddress), + MapEntry('mobileNumber', mobileNumber), + MapEntry('isdCode', isdCode), + MapEntry('isForSelf', isForSelf.toString()), + MapEntry('isDraft', 'true'), + MapEntry('isAddedToCart', 'true'), + ]); + + // Add optional address fields only if they are not null + if (address1 != null && address1.isNotEmpty) { + formData.fields.add(MapEntry('address1', address1)); + } + + if (address2 != null && address2.isNotEmpty) { + formData.fields.add(MapEntry('address2', address2)); + } + + // ⭐ Add postcard image file + final fileName = pcImageFile.path.split('/').last; + formData.files.add( + MapEntry( + 'pcImage', + await MultipartFile.fromFile( + pcImageFile.path, + filename: fileName, + ), + ), + ); + + if (kDebugMode) { + print('📤 [CREATE POSTCARD] ✅ Postcard Image File Added'); + print('📤 [CREATE POSTCARD] File Name: $fileName'); + print('📤 [CREATE POSTCARD] File Path: ${pcImageFile.path}'); + final fileSize = await pcImageFile.length(); + print('📤 [CREATE POSTCARD] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); + } + + // ⭐ Log complete payload details + log('📦 Request Payload Summary:'); + log('📦 Total Fields: ${formData.fields.length}'); + log('📦 Total Files: ${formData.files.length}'); + + log('📦 Field Details:'); + for (var field in formData.fields) { + log(' - ${field.key}: ${field.value}'); + } + + log('📦 File Details:'); + for (var file in formData.files) { + log(' - ${file.key}: ${file.value.filename} (${file.value.length} bytes)'); + } + + log('🌐 API URL: ${ApiUrls.createPostCard}'); + + // ⭐ Send as multipart/form-data + final response = await _apiServices.postApi( + url: ApiUrls.createPostCard, + data: formData, + ); + + log('✅ API Response Status: ${response.statusCode}'); + log('📥 API Response Data: ${response.data}'); + + if (kDebugMode) { + print('📤 [CREATE POSTCARD] ✅ Response Status: Success'); + print('📤 [CREATE POSTCARD] Full Response: ${response.data}'); + } + + return response.data as Map; + } catch (e, stackTrace) { + log( + '❌ createPostCard FAILED', + error: e, + stackTrace: stackTrace, + ); + throw Exception('Failed to create postcard: $e'); + } + } + + /// 🆕 Confirm Payment after successful Stripe payment + /// POST https://devapi.citycards.betadelivery.com/mobile/postcards/{postcardId}/confirm-payment + Future> confirmPayment({ + required int postcardId, + required String stripeStatus, + required String paymentStatus, + }) async { + try { + log('🟢 confirmPayment() called'); + log('📤 [CONFIRM PAYMENT] Postcard ID: $postcardId'); + log('📤 [CONFIRM PAYMENT] Stripe Status: $stripeStatus'); + log('📤 [CONFIRM PAYMENT] Payment Status: $paymentStatus'); + + // Construct URL with postcardId + final url = '${ApiUrls.baseUrl}/mobile/postcards/$postcardId/confirm-payment'; + + // Note: Update ApiUrls class if you want to use a constant instead + // final url = ApiUrls.confirmPayment(postcardId); + + if (kDebugMode) { + print('📤 [CONFIRM PAYMENT] API URL: $url'); + } + + // Request body + final requestBody = { + 'stripeStatus': stripeStatus, + 'paymentStatus': paymentStatus, + }; + + log('📦 Request Body: $requestBody'); + + // Send POST request + final response = await _apiServices.postApi( + url: url, + data: requestBody, + ); + + log('✅ [CONFIRM PAYMENT] Response Status: ${response.statusCode}'); + log('📥 [CONFIRM PAYMENT] Response Data: ${response.data}'); + + if (kDebugMode) { + print('📤 [CONFIRM PAYMENT] ✅ Payment confirmation successful'); + print('📤 [CONFIRM PAYMENT] Full Response: ${response.data}'); + } + + return response.data as Map; + } catch (e, stackTrace) { + log( + '❌ confirmPayment FAILED', + error: e, + stackTrace: stackTrace, + ); + throw Exception('Failed to confirm payment: $e'); + } + } +} \ No newline at end of file diff --git a/lib/postcard/repository/postcard_checkout_repository.dart b/lib/postcard/repository/postcard_checkout_repository.dart index 3d82050..a40e3b3 100644 --- a/lib/postcard/repository/postcard_checkout_repository.dart +++ b/lib/postcard/repository/postcard_checkout_repository.dart @@ -1,5 +1,6 @@ import 'dart:developer'; import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; @@ -9,132 +10,44 @@ import '../../networkApiServices/network_api_services.dart'; class CreatePostCardRepository { final NetworkApiService _apiServices = NetworkApiService(); - /// Create / Save Postcard (Draft or Final) - /// ⭐ UPDATED: Now uses multipart/form-data for file upload + /// ============================================================ + /// Create / Update Postcard (Draft or Final) + /// Uses multipart/form-data + /// URL requires pcId + /// ============================================================ + /// ============================================================ + /// Create / Update Postcard (Draft or Pay) + /// POST /mobile/postcards/{pcId}/draft-or-pay + /// Payload: { "isDraft": true/false } + /// ============================================================ Future> createPostCard({ - required String countryName, - required String cityName, - required String stateName, - required String zipCode, - - String? address1, // NOT required - String? address2, // NOT required - - required String pcTitle, - required String pcContent, - required File pcImageFile, // ⭐ CHANGED: File instead of String - required String pcNumber, - required String pcDatetime, - - required String fullname, - required String emailAddress, - required String mobileNumber, - required String isdCode, - - required bool isForSelf, + required int pcId, required bool isDraft, - - required double baseAmount, - required double totalTaxAmount, - required double totalAmount, }) async { try { log('🟡 createPostCard() called'); + log('🆔 Postcard ID: $pcId'); + log('📝 isDraft: $isDraft'); - if (kDebugMode) { - print('📤 [CREATE POSTCARD] Country: $countryName'); - print('📤 [CREATE POSTCARD] City: $cityName'); - print('📤 [CREATE POSTCARD] State: $stateName'); - print('📤 [CREATE POSTCARD] Zip: $zipCode'); - print('📤 [CREATE POSTCARD] Title: $pcTitle'); - print('📤 [CREATE POSTCARD] Number: $pcNumber'); - print('📤 [CREATE POSTCARD] Image File: ${pcImageFile.path}'); - print('📤 [CREATE POSTCARD] Is Draft: $isDraft'); - } + // ============================ + // API Call + // ============================ + final url = '${ApiUrls.baseUrl}/mobile/postcards/$pcId/draft-or-pay'; - // ⭐ Create FormData for multipart/form-data upload - final formData = FormData(); + final requestBody = { + 'isDraft': isDraft, + }; - // Add text fields - formData.fields.addAll([ - MapEntry('countryName', countryName), - MapEntry('cityName', cityName), - MapEntry('stateName', stateName), - MapEntry('zipCode', zipCode), - MapEntry('pcTitle', pcTitle), - MapEntry('pcContent', pcContent), - MapEntry('pcNumber', pcNumber), - MapEntry('pcDatetime', pcDatetime), - MapEntry('fullname', fullname), - MapEntry('emailAddress', emailAddress), - MapEntry('mobileNumber', mobileNumber), - MapEntry('isdCode', isdCode), - MapEntry('isForSelf', isForSelf.toString()), - MapEntry('isDraft', isDraft.toString()), - MapEntry('baseAmount', baseAmount.toString()), - MapEntry('totalTaxAmount', totalTaxAmount.toString()), - MapEntry('totalAmount', totalAmount.toString()), - ]); + log('🌐 API URL: $url'); + log('📦 Request Body: $requestBody'); - // Add optional address fields only if they are not null - if (address1 != null && address1.isNotEmpty) { - formData.fields.add(MapEntry('address1', address1)); - } - - if (address2 != null && address2.isNotEmpty) { - formData.fields.add(MapEntry('address2', address2)); - } - - // ⭐ Add postcard image file - final fileName = pcImageFile.path.split('/').last; - formData.files.add( - MapEntry( - 'pcImage', - await MultipartFile.fromFile( - pcImageFile.path, - filename: fileName, - ), - ), + final response = await _apiServices.putApi( + url: url, + data: requestBody, ); - if (kDebugMode) { - print('📤 [CREATE POSTCARD] ✅ Postcard Image File Added'); - print('📤 [CREATE POSTCARD] File Name: $fileName'); - print('📤 [CREATE POSTCARD] File Path: ${pcImageFile.path}'); - final fileSize = await pcImageFile.length(); - print('📤 [CREATE POSTCARD] File Size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); - } - - // ⭐ Log complete payload details - log('📦 Request Payload Summary:'); - log('📦 Total Fields: ${formData.fields.length}'); - log('📦 Total Files: ${formData.files.length}'); - - log('📦 Field Details:'); - for (var field in formData.fields) { - log(' - ${field.key}: ${field.value}'); - } - - log('📦 File Details:'); - for (var file in formData.files) { - log(' - ${file.key}: ${file.value.filename} (${file.value.length} bytes)'); - } - - log('🌐 API URL: ${ApiUrls.createPostCard}'); - - // ⭐ Send as multipart/form-data - final response = await _apiServices.postApi( - url: ApiUrls.createPostCard, - data: formData, - ); - - log('✅ API Response Status: ${response.statusCode}'); - log('📥 API Response Data: ${response.data}'); - - if (kDebugMode) { - print('📤 [CREATE POSTCARD] ✅ Response Status: Success'); - print('📤 [CREATE POSTCARD] Full Response: ${response.data}'); - } + log('✅ API Status: ${response.statusCode}'); + log('📥 API Response: ${response.data}'); return response.data as Map; } catch (e, stackTrace) { @@ -147,8 +60,10 @@ class CreatePostCardRepository { } } - /// 🆕 Confirm Payment after successful Stripe payment - /// POST https://devapi.citycards.betadelivery.com/mobile/postcards/{postcardId}/confirm-payment + /// ============================================================ + /// Confirm Stripe Payment + /// POST /mobile/postcards/{postcardId}/confirm-payment + /// ============================================================ Future> confirmPayment({ required int postcardId, required String stripeStatus, @@ -156,41 +71,26 @@ class CreatePostCardRepository { }) async { try { log('🟢 confirmPayment() called'); - log('📤 [CONFIRM PAYMENT] Postcard ID: $postcardId'); - log('📤 [CONFIRM PAYMENT] Stripe Status: $stripeStatus'); - log('📤 [CONFIRM PAYMENT] Payment Status: $paymentStatus'); + log('🆔 Postcard ID: $postcardId'); - // Construct URL with postcardId - final url = '${ApiUrls.baseUrl}/mobile/postcards/$postcardId/confirm-payment'; + final url = + '${ApiUrls.baseUrl}/mobile/postcards/$postcardId/confirm-payment'; - // Note: Update ApiUrls class if you want to use a constant instead - // final url = ApiUrls.confirmPayment(postcardId); - - if (kDebugMode) { - print('📤 [CONFIRM PAYMENT] API URL: $url'); - } - - // Request body final requestBody = { 'stripeStatus': stripeStatus, 'paymentStatus': paymentStatus, }; + log('🌐 API URL: $url'); log('📦 Request Body: $requestBody'); - // Send POST request final response = await _apiServices.postApi( url: url, data: requestBody, ); - log('✅ [CONFIRM PAYMENT] Response Status: ${response.statusCode}'); - log('📥 [CONFIRM PAYMENT] Response Data: ${response.data}'); - - if (kDebugMode) { - print('📤 [CONFIRM PAYMENT] ✅ Payment confirmation successful'); - print('📤 [CONFIRM PAYMENT] Full Response: ${response.data}'); - } + log('✅ Payment Confirmed: ${response.statusCode}'); + log('📥 Response: ${response.data}'); return response.data as Map; } catch (e, stackTrace) { @@ -202,4 +102,4 @@ class CreatePostCardRepository { throw Exception('Failed to confirm payment: $e'); } } -} \ No newline at end of file +} diff --git a/lib/postcard/views/add_filter_step_page_view.dart b/lib/postcard/views/add_filter_step_page_view.dart index 92386f6..15168e8 100644 --- a/lib/postcard/views/add_filter_step_page_view.dart +++ b/lib/postcard/views/add_filter_step_page_view.dart @@ -31,7 +31,27 @@ class AddFilterStepPageView extends StatelessWidget { children: [ CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true), StepProgressBar(totalSteps: 4, currentStep: 2), - const SizedBox(height: 24), + GestureDetector( + onTap: () { + context.read().add(GoToPreviousStep()); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + Icon(Icons.arrow_back, size: 20), + const SizedBox(width: 8), + Text( + "Back", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), Text( "Add a Filter", style: TextStyle( diff --git a/lib/postcard/views/my_postcard_preview_view.dart b/lib/postcard/views/my_postcard_preview_view.dart index 0d1e36a..0ae914a 100644 --- a/lib/postcard/views/my_postcard_preview_view.dart +++ b/lib/postcard/views/my_postcard_preview_view.dart @@ -42,59 +42,67 @@ class _MyPostcardPreviewViewState extends State { SizedBox(height: 29.h), // Postcard Number with Action Icons - Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "#${widget.postcard.pcNumber}", + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Row( + children: [ + /// PC Number (takes only available space) + Expanded( + child: Text( + widget.postcard.pcNumber, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: GoogleFonts.poppins( color: Colors.black, fontSize: 18.sp, fontWeight: FontWeight.w400, ), ), - Row( - children: [ - GestureDetector( - onTap: () { - // Delete functionality - }, - child: Image.asset( - 'assets/icons/delete_icon.png', - width: 24, - height: 24, - ), + ), + + SizedBox(width: 12.w), + + /// Action Icons + Row( + children: [ + GestureDetector( + onTap: () { + // Delete functionality + }, + child: Image.asset( + 'assets/icons/delete_icon.png', + width: 24, + height: 24, ), - SizedBox(width: 16.w), - GestureDetector( - onTap: () { - // Edit functionality - }, - child: Image.asset( - 'assets/icons/edit_icon.png', - width: 24, - height: 24, - ), + ), + SizedBox(width: 16.w), + GestureDetector( + onTap: () { + // Edit functionality + }, + child: Image.asset( + 'assets/icons/edit_icon.png', + width: 24, + height: 24, ), - SizedBox(width: 16.w), - GestureDetector( - onTap: () { - // Send functionality - }, - child: Image.asset( - 'assets/icons/send_icon.png', - width: 24, - height: 24, - ), + ), + SizedBox(width: 16.w), + GestureDetector( + onTap: () { + // Send functionality + }, + child: Image.asset( + 'assets/icons/send_icon.png', + width: 24, + height: 24, ), - ], - ), - ], - ), + ), + ], + ), + ], ), - SizedBox(height: 20.h), + ), + SizedBox(height: 20.h), // Flip buttons Padding( @@ -112,14 +120,14 @@ class _MyPostcardPreviewViewState extends State { children: [ Icon( Icons.arrow_back, - color: !showBack ? const Color(0xffF95F62) : Colors.grey[400], + color: !showBack ? Colors.grey[400] : const Color(0xffF95F62), size: 20, ), SizedBox(width: 6.w), Text( 'Flip', style: GoogleFonts.poppins( - color: !showBack ? const Color(0xffF95F62) : Colors.grey[400], + color: !showBack ? Colors.grey[400] : const Color(0xffF95F62), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -138,7 +146,7 @@ class _MyPostcardPreviewViewState extends State { Text( 'Flip', style: GoogleFonts.poppins( - color: showBack ? const Color(0xffF95F62) : Colors.grey[400], + color: showBack ? Colors.grey[400] : const Color(0xffF95F62), fontSize: 14.sp, fontWeight: FontWeight.w500, ), @@ -146,7 +154,7 @@ class _MyPostcardPreviewViewState extends State { SizedBox(width: 6.w), Icon( Icons.arrow_forward, - color: showBack ? const Color(0xffF95F62) : Colors.grey[400], + color: showBack ? Colors.grey[400] : const Color(0xffF95F62), size: 20, ), ], diff --git a/lib/postcard/views/order_success_page_view.dart b/lib/postcard/views/order_success_page_view.dart index 21c5962..c581983 100644 --- a/lib/postcard/views/order_success_page_view.dart +++ b/lib/postcard/views/order_success_page_view.dart @@ -58,7 +58,7 @@ class OrderSuccessPageView extends StatelessWidget { text: "Your order has been placed. Your order\nid is ", ), TextSpan( - text: "#${state.pcNumber ?? 'N/A'}", // 🆕 USE DYNAMIC VALUE + text: state.pcNumber ?? 'N/A', // 🆕 USE DYNAMIC VALUE style: const TextStyle( fontWeight: FontWeight.w600, color: Color(0xff585858), @@ -86,9 +86,13 @@ class OrderSuccessPageView extends StatelessWidget { angle: 0.20, child: BackCardWidget( message: state.message ?? "", - state: "State", - country: "country", - city: "City", + state: state.state??"", + country: state.country??"", + city: state.city??"", + selectedFont: state.selectedFont, + pincode: state.zipCode??"", + name: state.fullName??"", + address: state.address, key: const ValueKey('back'), // selectedFont: state.selectedFont, ), diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart index 443cb12..99d7f40 100644 --- a/lib/postcard/views/postcard_checkout_page_view.dart +++ b/lib/postcard/views/postcard_checkout_page_view.dart @@ -41,6 +41,7 @@ class PostcardCheckoutPageView extends StatefulWidget { final double baseAmount; final double totalTaxAmount; final double totalAmount; + final int? postcardId; const PostcardCheckoutPageView({ super.key, @@ -61,6 +62,7 @@ class PostcardCheckoutPageView extends StatefulWidget { required this.baseAmount, required this.totalTaxAmount, required this.totalAmount, + required this.postcardId, }); @override @@ -102,6 +104,7 @@ class _PostcardCheckoutPageViewState extends State { baseAmount: widget.baseAmount, totalTaxAmount: widget.totalTaxAmount, totalAmount: widget.totalAmount, + postcardId: widget.postcardId, ), ); }); @@ -302,13 +305,13 @@ class _PostcardCheckoutPageViewState extends State { ), ); - final bloc = context.read(); - bloc.add( - ConfirmPaymentEvent( - stripeStatus: 'requires_payment_method', - paymentStatus: 'failed', - ), - ); + // final bloc = context.read(); + // bloc.add( + // ConfirmPaymentEvent( + // stripeStatus: 'requires_payment_method', + // paymentStatus: 'failed', + // ), + // ); } } @@ -382,7 +385,27 @@ class _PostcardCheckoutPageViewState extends State { isProfilePage: false, showDivider: true, ), - // Header + GestureDetector( + onTap: () { + context.read().add(GoToPreviousStep()); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon(Icons.arrow_back, size: 20), + const SizedBox(width: 8), + Text( + "Back", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -426,6 +449,7 @@ class _PostcardCheckoutPageViewState extends State { address: creationState.address, name: widget.fullname, pincode: widget.zipCode, + selectedFont: creationState.selectedFont, key: const ValueKey('back'), // selectedFont: creationState.selectedFont, ), diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart index b241ada..92b535c 100644 --- a/lib/postcard/views/postcard_creation_page_view.dart +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -7,10 +7,12 @@ import 'package:citycards_customer/postcard/views/upload_photo_step_page_view.da import 'package:citycards_customer/postcard/views/write_message_step_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - +import '../blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart'; +import '../blocs/addToCartPostcard/add_to_cart_postcard_state.dart'; import '../blocs/postcardCheckout/postcard_checkout_bloc.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_state.dart'; +import '../repository/postcard_add_to_cart_repository.dart'; import '../repository/postcard_checkout_repository.dart'; import 'my_postcards_view.dart'; import 'order_success_page_view.dart'; @@ -20,8 +22,17 @@ class PostcardCreationPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => PostcardCreationBloc(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => PostcardCreationBloc(), + ), + BlocProvider( + create: (_) => AddToCartPostCardBloc( + AddToCartPostCardRepository(), + ), + ), + ], child: BlocBuilder( builder: (context, state) { Widget stepWidget; @@ -39,9 +50,40 @@ class PostcardCreationPage extends StatelessWidget { stepWidget = const PreviewPostcardStepPageView(); break; case PostcardStep.purchase: - stepWidget = const PostcardPurchaseFormPageView(); + // If buying for myself (isGift = false), use user profile data + // Otherwise, leave fields empty for gift recipient + stepWidget = PostcardPurchaseFormPageView( + initialFullName: !state.isGift ? state.userProfileFullName : null, + initialEmail: !state.isGift ? state.userProfileEmail : null, + initialPhone: !state.isGift ? state.userProfilePhone : null, + initialAddress: !state.isGift ? state.userProfileAddress : null, + initialCity: !state.isGift ? state.userProfileCity : null, + initialState: !state.isGift ? state.userProfileState : null, + initialZipCode: !state.isGift ? state.userProfileZipCode : null, + initialCountry: !state.isGift ? state.userProfileCountry : null, + ); break; case PostcardStep.checkout: + // Get the cart state to access response data + final cartState = context.read().state; + + // Extract values from the cart response or use defaults + String pcNumber = '12'; + String pcDatetime = ''; + double baseAmount = 50; + double totalTaxAmount = 20; + double totalAmount = 30; + int? postcardId; + + if (cartState is AddToCartPostCardSuccess) { + pcNumber = cartState.pcNumber; + pcDatetime = cartState.pcDatetime; + baseAmount = cartState.baseAmount; + totalTaxAmount = cartState.totalTaxAmount; + totalAmount = cartState.totalAmount; + postcardId = cartState.postcardId; + } + stepWidget = BlocProvider( create: (_) => PostcardCheckoutBloc( repository: CreatePostCardRepository(), @@ -51,17 +93,20 @@ class PostcardCreationPage extends StatelessWidget { cityName: state.city ?? 'N/A', stateName: state.state ?? 'N/A', zipCode: state.zipCode ?? 'N/A', + address1: state.address, // ✅ Add this + address2: '', // ✅ Add this (or pass actual value if you have it) pcTitle: state.pcTitle ?? 'N/A', - pcNumber: '12', - pcDatetime: '2008-11-20', + pcNumber: pcNumber, + pcDatetime: pcDatetime, fullname: state.fullName ?? 'N/A', emailAddress: state.emailId ?? 'N/A', mobileNumber: state.phoneNumber ?? 'N/A', isdCode: '+91', isForSelf: !state.isGift, - totalTaxAmount: 20, - baseAmount: 50, - totalAmount: 30, + totalTaxAmount: totalTaxAmount, + baseAmount: baseAmount, + totalAmount: totalAmount, + postcardId: postcardId, ), ); break; @@ -74,7 +119,7 @@ class PostcardCreationPage extends StatelessWidget { break; case PostcardStep.myOrderPostcardPreview: stepWidget = const OrderPostcardPreviewPageView(); - } + } return Scaffold( backgroundColor: Colors.white, @@ -84,4 +129,4 @@ class PostcardCreationPage extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart index e41f101..04bb216 100644 --- a/lib/postcard/views/postcard_purchase_form_page_view.dart +++ b/lib/postcard/views/postcard_purchase_form_page_view.dart @@ -3,13 +3,36 @@ 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 'package:intl/intl.dart'; import '../../common_packages/app_bar.dart'; +import '../blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart'; +import '../blocs/addToCartPostcard/add_to_cart_postcard_event.dart'; +import '../blocs/addToCartPostcard/add_to_cart_postcard_state.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_events.dart'; import '../blocs/postcard_creation_state.dart'; class PostcardPurchaseFormPageView extends StatefulWidget { - const PostcardPurchaseFormPageView({super.key}); + final String? initialFullName; + final String? initialEmail; + final String? initialPhone; + final String? initialAddress; + final String? initialCity; + final String? initialState; + final String? initialZipCode; + final String? initialCountry; + + const PostcardPurchaseFormPageView({ + super.key, + this.initialFullName, + this.initialEmail, + this.initialPhone, + this.initialAddress, + this.initialCity, + this.initialState, + this.initialZipCode, + this.initialCountry, + }); @override State createState() => _PostcardPurchaseFormPageViewState(); @@ -30,6 +53,20 @@ class _PostcardPurchaseFormPageViewState extends State( builder: (context, state) { - final bloc = context.read(); + final creationBloc = context.read(); - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar( - isWhiteLogo: false, - isProfilePage: false, - showDivider: true, - ), - const SizedBox(height: 20), - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: state.imagePath != null - ? Image.file( - File(state.imagePath!), - height: 70, - width: 70, - fit: BoxFit.cover, - ) - : Container( - height: 70, - width: 70, - color: const Color(0xffFEE7E7), - child: const Icon(Icons.image_outlined, - color: Color(0xffFDCDCE)), + return BlocListener( + listener: (context, cartState) { + if (cartState is AddToCartPostCardSuccess) { + // Update the postcard number in creation bloc + creationBloc.add(UpdatePostcardNumber(cartState.pcNumber)); + + // Navigate to next step (checkout) + creationBloc.add(GoToNextStep()); + } else if (cartState is AddToCartPostCardFailure) { + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(cartState.message), + backgroundColor: Colors.red, + ), + ); + } + }, + child: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + GestureDetector( + onTap: () { + context.read().add(GoToPreviousStep()); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + Icon(Icons.arrow_back, size: 20), + const SizedBox(width: 8), + Text( + "Back", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), - const SizedBox(width: 16), - Expanded( - child: TextFormField( - controller: _titleController, - decoration: InputDecoration( - hintText: "Add title", - hintStyle: GoogleFonts.poppins( - color: const Color(0xff999999), fontSize: 14.sp), - enabledBorder: const UnderlineInputBorder( - borderSide: - BorderSide(color: Color(0xffFDCDCE), width: 1), + ), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: state.imagePath != null + ? Image.file( + File(state.imagePath!), + height: 70, + width: 70, + fit: BoxFit.cover, + ) + : Container( + height: 70, + width: 70, + color: const Color(0xffFEE7E7), + child: const Icon(Icons.image_outlined, + color: Color(0xffFDCDCE)), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _titleController, + decoration: InputDecoration( + hintText: "Add title", + hintStyle: GoogleFonts.poppins( + color: const Color(0xff999999), fontSize: 14.sp), + enabledBorder: const UnderlineInputBorder( + borderSide: + BorderSide(color: Color(0xffFDCDCE), width: 1), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: + BorderSide(color: Color(0xffFDCDCE), width: 1), + ), ), - focusedBorder: const UnderlineInputBorder( - borderSide: - BorderSide(color: Color(0xffFDCDCE), width: 1), + style: GoogleFonts.poppins(fontSize: 14.sp), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a title'; + } + return null; + }, + ), + ), + ], + ), + + const SizedBox(height: 28), + + // Personal details section + Text( + "Recipient Details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 16), + + _buildInputField( + label: "Recipient", + hint: "Enter the recipient's name", + controller: _fullNameController, + ), + _buildInputField( + label: "Email", + hint: "eg: Jay@gmail.com", + controller: _emailController, + keyboardType: TextInputType.emailAddress, + ), + _buildInputField( + label: "Phone number", + hint: "eg: +91 9999 999 999", + controller: _phoneController, + keyboardType: TextInputType.phone, + ), + _buildInputField( + label: "Address", + hint: "Enter the recipient's Address", + controller: _addressController, + ), + _buildInputField( + label: "City", + hint: "Enter the name of your city", + controller: _cityController, + ), + _buildDropdownField( + label: "State", + hint: "Select your state", + value: _selectedState, + onChanged: (val) { + setState(() { + _selectedState = val; + }); + }, + ), + _buildInputField( + label: "Zip Code", + hint: "Enter the Zip Code you reside in", + controller: _zipCodeController, + keyboardType: TextInputType.number, + ), + _buildDropdownField( + label: "Country", + hint: "Select your country", + value: _selectedCountry, + onChanged: (val) { + setState(() { + _selectedCountry = val; + }); + }, + ), + + const SizedBox(height: 24), + + // Next button + BlocBuilder( + builder: (context, cartState) { + final isLoading = cartState is AddToCartPostCardLoading; + final addToCartBloc = context.read(); + + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isLoading + ? null + : () { + creationBloc.add( + UpdatePurchaseFormData( + pcTitle: _titleController.text, + fullName: _fullNameController.text, + emailId: _emailController.text, + phoneNumber: _phoneController.text, + address: _addressController.text, + city: _cityController.text, + state: _selectedState, + zipCode: _zipCodeController.text, + country: _selectedCountry, + ), + ); + if (_formKey.currentState!.validate()) { + final currentDate = DateFormat('yyyy-MM-dd').format(DateTime.now()); + + addToCartBloc.add( + AddToCartPostCardRequested( + countryName: _selectedCountry ?? '', + cityName: _cityController.text, + stateName: _selectedState ?? '', + zipCode: _zipCodeController.text, + address1: _addressController.text, + address2: null, + pcTitle: _titleController.text, + pcContent: creationBloc.getFormattedMessage(), + pcImageFile: File(state.imagePath!), + pcNumber: '12', + pcDatetime: currentDate, + fullname: _fullNameController.text, + emailAddress: _emailController.text, + mobileNumber: _phoneController.text, + isdCode: '+91', + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), ), ), - style: GoogleFonts.poppins(fontSize: 14.sp), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a title'; - } - return null; - }, - ), - ), - ], - ), - - const SizedBox(height: 28), - - // Personal details section - Text( - "Recipient Details", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: const Color(0xff1A1A1A), - ), - ), - const SizedBox(height: 16), - - _buildInputField( - label: "Recipient", - hint: "Enter the recipient's name", - controller: _fullNameController, - ), - _buildInputField( - label: "Email", - hint: "eg: Jay@gmail.com", - controller: _emailController, - keyboardType: TextInputType.emailAddress, - ), - _buildInputField( - label: "Phone number", - hint: "eg: +91 9999 999 999", - controller: _phoneController, - keyboardType: TextInputType.phone, - ), - _buildInputField( - label: "Address", - hint: "Enter the recipient's Address", - controller: _addressController, - ), - _buildInputField( - label: "City", - hint: "Enter the name of your city", - controller: _cityController, - ), - _buildDropdownField( - label: "State", - hint: "Select your state", - value: _selectedState, - onChanged: (val) { - setState(() { - _selectedState = val; - }); - }, - ), - _buildInputField( - label: "Zip Code", - hint: "Enter the Zip Code you reside in", - controller: _zipCodeController, - keyboardType: TextInputType.number, - ), - _buildDropdownField( - label: "Country", - hint: "Select your country", - value: _selectedCountry, - onChanged: (val) { - setState(() { - _selectedCountry = val; - }); - }, - ), - const SizedBox(height: 30), - - // Next Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - // Update the bloc with form data - bloc.add(UpdatePurchaseFormData( - pcTitle: _titleController.text, - fullName: _fullNameController.text, - emailId: _emailController.text, - phoneNumber: _phoneController.text, - address: _addressController.text, - city: _cityController.text, - country: _selectedCountry ?? '', - state: _selectedState ?? '', - zipCode: _zipCodeController.text, - )); - - // Navigate to next step - bloc.add(GoToNextStep()); - } + ); }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), - ), - ), - child: Text( - "Next", - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, - ), - ), ), - ), - ], + ], + ), ), ), ), @@ -347,9 +461,23 @@ class _PostcardPurchaseFormPageViewState extends State().add(GoToPreviousStep()); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + Icon(Icons.arrow_back, size: 20), + const SizedBox(width: 8), + Text( + "Back", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), Text( "Preview your Postcard", @@ -103,6 +124,7 @@ class _PreviewPostcardStepPageViewState extends State { children: [ CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), StepProgressBar(totalSteps: 4, currentStep: 3), - const SizedBox(height: 24), + GestureDetector( + onTap: () { + context.read().add(GoToPreviousStep()); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + Icon(Icons.arrow_back, size: 20), + const SizedBox(width: 8), + Text( + "Back", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), Text("Write a message", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)), diff --git a/lib/postcard/widgets/back_card_widget.dart b/lib/postcard/widgets/back_card_widget.dart index 2009c15..1db35ed 100644 --- a/lib/postcard/widgets/back_card_widget.dart +++ b/lib/postcard/widgets/back_card_widget.dart @@ -5,6 +5,7 @@ import 'package:html/parser.dart' as html_parser; class BackCardWidget extends StatelessWidget { final String message; + final String? selectedFont; final String name; final String address; final String city; @@ -17,6 +18,7 @@ class BackCardWidget extends StatelessWidget { const BackCardWidget({ super.key, this.message = '', + this.selectedFont, this.name = '', this.address = '', this.city = '', @@ -98,6 +100,14 @@ class BackCardWidget extends StatelessWidget { final messageText = parsedMessage['text'] ?? ''; final fontFamily = parsedMessage['fontFamily'] ?? ''; + // Determine which font to use: selectedFont takes priority, then parsed fontFamily, then default + String finalFontFamily = ''; + if (selectedFont != null && selectedFont!.isNotEmpty) { + finalFontFamily = selectedFont!; + } else if (fontFamily.isNotEmpty) { + finalFontFamily = fontFamily; + } + return Transform.scale( scale: scale, child: Container( @@ -129,7 +139,7 @@ class BackCardWidget extends StatelessWidget { child: SingleChildScrollView( child: Text( messageText, - style: _getFontStyle(fontFamily, 16.sp, 1.7), + style: _getFontStyle(finalFontFamily, 16.sp, 1.7), ), ), ), @@ -192,28 +202,28 @@ class BackCardWidget extends StatelessWidget { SizedBox(height: 5.h), if (name.isNotEmpty) ...[ _addressLine(name), - _divider(), ], + _divider(), if (address.isNotEmpty) ...[ _addressLine(address), - _divider(), ], + _divider(), if (city.isNotEmpty) ...[ _addressLine(city), _divider(), ], if (state.isNotEmpty) ...[ _addressLine(state), - _divider(), ], + _divider(), if (country.isNotEmpty) ...[ _addressLine(country), - _divider(), ], + _divider(), if (pincode.isNotEmpty) ...[ _addressLine(pincode), - _divider(), ], + _divider(), ], ), ), diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart index 9f3f696..fefa473 100644 --- a/lib/postcard/widgets/purchase_details_bottom_sheet.dart +++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart @@ -219,6 +219,21 @@ class PurchaseDetailsBottomSheet { width: double.infinity, child: ElevatedButton( onPressed: () { + // If buying for myself, store the profile data + if (!postcardState.isGift && purchaseState.profile != null) { + final profile = purchaseState.profile!; + postcardBloc.add(StoreUserProfileData( + fullName: "${profile.firstName ?? ''} ${profile.lastName ?? ''}".trim(), + email: profile.emailAddress, + phone: profile.mobileNumber, + address: "${profile.address1 ?? ''} ${profile.address2 ?? ''}".trim(), + city: profile.cityName, + state: profile.stateName, + zipCode: profile.zipCode, + country: profile.country, + )); + } + PurchaseDetailsBottomSheet.close(context); postcardBloc.add(GoToNextStep()); }, diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart index 718d451..4eb27c1 100644 --- a/lib/profile/view/edit_profile/edit_profile_view.dart +++ b/lib/profile/view/edit_profile/edit_profile_view.dart @@ -30,11 +30,13 @@ 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(); + // Dropdown values + String? selectedState; + String? selectedCountry; + final _formKey = GlobalKey(); final ImagePicker _picker = ImagePicker(); @@ -68,11 +70,15 @@ 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 ?? ''; + // Set dropdown values from fetched data + setState(() { + selectedState = profile.stateName; + selectedCountry = profile.country; + }); + // ⭐ REMOVED setState - image is now managed by BLoC state if (kDebugMode && profile.profileImage != null && profile.profileImage!.isNotEmpty) { print('🔵 [EDIT PROFILE] ✅ Current profile image URL: ${profile.profileImage}'); @@ -329,16 +335,12 @@ class _EditProfilePageState extends State { address2: address2Controller.text.trim().isEmpty ? null : address2Controller.text.trim(), - // ⭐ ADD THESE NEW FIELDS + // ⭐ UPDATED: Use dropdown values instead of controllers 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(), + state: selectedState, + country: selectedCountry, postalCode: zipCodeController.text.trim().isEmpty ? null : zipCodeController.text.trim(), @@ -354,8 +356,6 @@ class _EditProfilePageState extends State { phoneController.dispose(); address1Controller.dispose(); address2Controller.dispose(); - stateController.dispose(); - countryController.dispose(); cityController.dispose(); zipCodeController.dispose(); super.dispose(); @@ -538,22 +538,127 @@ 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: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText(text: "State", size: 14.sp), + SizedBox(height: 6.h), + Container( + height: 42.h, + padding: EdgeInsets.symmetric(horizontal: 24.w), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: const Color(0xBBC83B61).withOpacity(0.4), + width: 0.4.w, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedState, + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFF8E8E8E), + ), + hint: Text( + "Select state", + style: TextStyle( + fontSize: 12.sp, + color: const Color(0xFF8E8E8E), + ), + ), + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF2D3134), + ), + onChanged: isLoading ? null : (value) { + setState(() { + selectedState = value; + }); + }, + items: [ + "New South Wales", + "Victoria", + "Queensland", + "South Australia", + "Western Australia", + "Tasmania", + "Northern Territory", + "Australian Capital Territory" + ].map((value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle(fontSize: 14.sp), + ), + ); + }).toList(), + ), + ), + ), + ], ), ), Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0.w), - child: CustomTextField( - label: "Country", - hint: "Select your Country", - controller: countryController, - enabled: !isLoading, + padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText(text: "Country", size: 14.sp), + SizedBox(height: 6.h), + Container( + height: 42.h, + padding: EdgeInsets.symmetric(horizontal: 24.w), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: const Color(0xBBC83B61).withOpacity(0.4), + width: 0.4.w, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedCountry, + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFF8E8E8E), + ), + hint: Text( + "Select country", + style: TextStyle( + fontSize: 12.sp, + color: const Color(0xFF8E8E8E), + ), + ), + style: TextStyle( + fontSize: 14.sp, + color: const Color(0xFF2D3134), + ), + onChanged: isLoading ? null : (value) { + setState(() { + selectedCountry = value; + }); + }, + items: ["Australia"].map((value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle(fontSize: 14.sp), + ), + ); + }).toList(), + ), + ), + ), + ], ), ), diff --git a/lib/search_offers/model/offers_model.dart b/lib/search_offers/model/offers_model.dart index f7d7ef2..e3ae508 100644 --- a/lib/search_offers/model/offers_model.dart +++ b/lib/search_offers/model/offers_model.dart @@ -73,7 +73,7 @@ class Offer { factory Offer.fromJson(Map json) { return Offer( - id: json['id'], + id: json['id'] ?? 0, title: json['title'] ?? '', description: json['description'] ?? '', offerCode: json['offerCode'] ?? '', @@ -133,7 +133,7 @@ class City { factory City.fromJson(Map json) { return City( - id: json['id'], + id: json['id'] ?? 0, name: json['name'] ?? '', ); } @@ -151,8 +151,8 @@ class City { class CardInfo { final int id; final String title; - final int adultPrice; - final int childPrice; + final num adultPrice; + final num childPrice; CardInfo({ required this.id, @@ -163,7 +163,7 @@ class CardInfo { factory CardInfo.fromJson(Map json) { return CardInfo( - id: json['id'], + id: json['id'] ?? 0, title: json['title'] ?? '', adultPrice: json['adultPrice'] ?? 0, childPrice: json['childPrice'] ?? 0, @@ -193,7 +193,7 @@ class CardType { factory CardType.fromJson(Map json) { return CardType( - id: json['id'], + id: json['id'] ?? 0, displayName: json['displayName'] ?? '', ); } @@ -219,7 +219,7 @@ class Category { factory Category.fromJson(Map json) { return Category( - id: json['id'], + id: json['id'] ?? 0, categoryName: json['categoryName'] ?? '', ); } @@ -230,4 +230,4 @@ class Category { 'categoryName': categoryName, }; } -} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index c3d6afb..3808f9c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + csc_picker_plus: + dependency: "direct main" + description: + name: csc_picker_plus + sha256: "105e1989dd7462a504d60af024880918bb2936dbb9c97f46c4bd4923fe011411" + url: "https://pub.dev" + source: hosted + version: "0.0.3" csslib: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b87fc0e..d2153f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: geocoding: ^4.0.0 cached_network_image: ^3.4.1 bloc: ^9.2.0 + csc_picker_plus: ^0.0.3 dev_dependencies: flutter_test: From 48fd7037ea3d3898c629c04647ba61e385be82c8 Mon Sep 17 00:00:00 2001 From: mystery012728 Date: Fri, 13 Feb 2026 18:34:00 +0530 Subject: [PATCH 8/8] snack bar bug solved --- lib/core/global_keys.dart | 6 ++++++ lib/login/view/login_email_bottomsheet.dart | 1 + lib/login/view/verify_otp_bottomsheet.dart | 1 + lib/main.dart | 2 ++ 4 files changed, 10 insertions(+) create mode 100644 lib/core/global_keys.dart diff --git a/lib/core/global_keys.dart b/lib/core/global_keys.dart new file mode 100644 index 0000000..8ae3c31 --- /dev/null +++ b/lib/core/global_keys.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +class GlobalKeys { + static final GlobalKey scaffoldMessengerKey = + GlobalKey(); +} \ No newline at end of file diff --git a/lib/login/view/login_email_bottomsheet.dart b/lib/login/view/login_email_bottomsheet.dart index 57cbecf..43675a9 100644 --- a/lib/login/view/login_email_bottomsheet.dart +++ b/lib/login/view/login_email_bottomsheet.dart @@ -52,6 +52,7 @@ class _LoginEmailBottomsheetState extends State { ), ); } else if (state is LoginError) { + Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.errorMessage), diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart index 7269a30..e93986c 100644 --- a/lib/login/view/verify_otp_bottomsheet.dart +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -77,6 +77,7 @@ class _VerifyOtpBottomsheetState extends State { ), ); } else if (state is VerifyOtpError) { + Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.errorMessage), diff --git a/lib/main.dart b/lib/main.dart index 30028fe..a125755 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; // ADD THIS import 'cart/blocs/myPassCart/my_pass_cart_bloc.dart'; import 'core/app_router.dart'; +import 'core/global_keys.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_bloc.dart'; import 'home/bloc/FirstTimeUserHome/first_time_user_home_event.dart'; import 'home/bloc/registeredHome/home_bloc.dart'; @@ -99,6 +100,7 @@ class MyApp extends StatelessWidget { ) ], child: MaterialApp( + scaffoldMessengerKey: GlobalKeys.scaffoldMessengerKey, onGenerateRoute: _appRouter.onGenerateRoute, initialRoute: RouteConstants.splash, debugShowCheckedModeBanner: false,