diff --git a/assets/icons/calendar.png b/assets/icons/calendar.png new file mode 100644 index 0000000..c25aff6 Binary files /dev/null and b/assets/icons/calendar.png differ diff --git a/assets/icons/person.png b/assets/icons/person.png new file mode 100644 index 0000000..81d29c0 Binary files /dev/null and b/assets/icons/person.png differ diff --git a/assets/icons/time.png b/assets/icons/time.png new file mode 100644 index 0000000..8042849 Binary files /dev/null and b/assets/icons/time.png differ diff --git a/lib/StripePayment/bloc/stripe_payment_bloc.dart b/lib/StripePayment/bloc/stripe_payment_bloc.dart index 9509b3d..79b166f 100644 --- a/lib/StripePayment/bloc/stripe_payment_bloc.dart +++ b/lib/StripePayment/bloc/stripe_payment_bloc.dart @@ -9,6 +9,9 @@ import 'stripe_payment_state.dart'; class StripePaymentBloc extends Bloc { 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: