diff --git a/README.md b/README.md index 3f2cf54..fcb7f41 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A few resources to get you started if this is your first Flutter project: - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, +[online documentation](https://docs.flutter.dev/),which offers tutorials, samples, guidance on mobile development, and a full API reference.

Figma Link

diff --git a/assets/images/empty_postcard_drafts.png b/assets/images/empty_postcard_drafts.png new file mode 100644 index 0000000..5f71aae Binary files /dev/null and b/assets/images/empty_postcard_drafts.png differ diff --git a/assets/images/empty_postcard_orders.png b/assets/images/empty_postcard_orders.png new file mode 100644 index 0000000..76f34b9 Binary files /dev/null and b/assets/images/empty_postcard_orders.png differ diff --git a/lib/StripePayment/bloc/stripe_payment_bloc.dart b/lib/StripePayment/bloc/stripe_payment_bloc.dart index 39e336e..4b63321 100644 --- a/lib/StripePayment/bloc/stripe_payment_bloc.dart +++ b/lib/StripePayment/bloc/stripe_payment_bloc.dart @@ -14,6 +14,7 @@ class StripePaymentBloc extends Bloc { }) : _stripeService = stripeService ?? StripeService(), super(const StripePaymentInitial()) { on(_onInitiatePayment); + on(_onInitiatePaymentWithClientSecret); on(_onResetPaymentState); } @@ -66,6 +67,46 @@ class StripePaymentBloc extends Bloc { } } + /// 🆕 NEW: Handle payment with clientSecret directly from backend + Future _onInitiatePaymentWithClientSecret( + InitiatePaymentWithClientSecret event, + Emitter emit, + ) async { + try { + emit(const StripePaymentLoading()); + + // 1ī¸âƒŖ Init Payment Sheet with clientSecret from backend + await Stripe.instance.initPaymentSheet( + paymentSheetParameters: SetupPaymentSheetParameters( + paymentIntentClientSecret: event.clientSecret, + merchantDisplayName: "CityCards", + style: ThemeMode.light, + ), + ); + + // 2ī¸âƒŖ Show Payment Sheet + await Stripe.instance.presentPaymentSheet(); + + // ✅ SUCCESS + emit(const StripePaymentSuccess()); + } on StripeException catch (e) { + // Handle Stripe-specific errors + if (e.error.code == FailureCode.Canceled) { + emit(StripePaymentCancelled( + message: e.error.localizedMessage ?? 'Payment Cancelled', + )); + } else { + emit(StripePaymentFailure( + error: e.error.localizedMessage ?? 'Payment failed', + )); + } + } catch (e) { + emit(StripePaymentFailure( + error: e.toString(), + )); + } + } + void _onResetPaymentState( ResetPaymentState event, Emitter emit, diff --git a/lib/StripePayment/bloc/stripe_payment_event.dart b/lib/StripePayment/bloc/stripe_payment_event.dart index f356b54..470e359 100644 --- a/lib/StripePayment/bloc/stripe_payment_event.dart +++ b/lib/StripePayment/bloc/stripe_payment_event.dart @@ -20,6 +20,18 @@ class InitiatePayment extends StripePaymentEvent { List get props => [amount, currency]; } +/// 🆕 NEW: Event to initiate payment with clientSecret from backend +class InitiatePaymentWithClientSecret extends StripePaymentEvent { + final String clientSecret; + + const InitiatePaymentWithClientSecret({ + required this.clientSecret, + }); + + @override + List get props => [clientSecret]; +} + class ResetPaymentState extends StripePaymentEvent { const ResetPaymentState(); } \ No newline at end of file diff --git a/lib/StripePayment/repository/stripe_service.dart b/lib/StripePayment/repository/stripe_service.dart index dddd3d1..312daee 100644 --- a/lib/StripePayment/repository/stripe_service.dart +++ b/lib/StripePayment/repository/stripe_service.dart @@ -11,7 +11,7 @@ class StripeService { // âš ī¸ TEMPORARY FALLBACK - Use secret key directly // TODO: Remove this and use backend when ready! - final String _stripeSecretKey = 'sk_test_51SrwZ7RtCkWyT4EmgS97odPlrKNj2TUxIkyu5L2i6qQyEpCivhYtEO6cW660UjBMoUsN1rUldvVhGx7RpGMarANp00Ntyi2Bp4'; // ← ADD YOUR SECRET KEY + final String _stripeSecretKey = ''; // ← ADD YOUR SECRET KEY Future createPaymentIntent({ required int amount, diff --git a/lib/buy_a_pass/view/buy_pass_view.dart b/lib/buy_a_pass/view/buy_pass_view.dart index 3ffd6e0..c01845a 100644 --- a/lib/buy_a_pass/view/buy_pass_view.dart +++ b/lib/buy_a_pass/view/buy_pass_view.dart @@ -240,102 +240,110 @@ class BuyPassContent extends StatelessWidget { itemBuilder: (context, index) { final offer = selectedCard.offers[index]; - return Container( - padding: EdgeInsets.symmetric( - horizontal: 6.w, - vertical: 6.h, - ), - decoration: BoxDecoration( - border: Border.all( - color: const Color(0xFFF95F62).withOpacity(.24), + return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.offerPassDetail, + arguments: offer.id, // ✅ pass offerId + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, + vertical: 6.h, ), - borderRadius: BorderRadius.circular(12.sp), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - /// Image - ClipRRect( - borderRadius: BorderRadius.circular(8.sp), - child: offer.mobileBannerImage != null && - offer.mobileBannerImage!.isNotEmpty - ? Image.network( - '${ApiUrls.baseUrl}/${offer.mobileBannerImage}', - width: double.infinity, - height: 120.5.h, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - width: double.infinity, - height: 120.5.h, - color: const Color(0xFFFEE7E7), - child: Icon( - Icons.local_offer, - size: 40.sp, - color: - const Color(0xFFF95F62).withOpacity(.6), - ), - ); - }, - loadingBuilder: - (context, child, loadingProgress) { - if (loadingProgress == null) return child; - - return Container( - width: double.infinity, - height: 120.5.h, - color: const Color(0xFFFEE7E7), - child: Center( - child: CircularProgressIndicator( - strokeWidth: 2, - color: const Color(0xFFF95F62), - value: loadingProgress - .expectedTotalBytes != - null - ? loadingProgress - .cumulativeBytesLoaded / - loadingProgress - .expectedTotalBytes! - : null, + decoration: BoxDecoration( + border: Border.all( + color: const Color(0xFFF95F62).withOpacity(.24), + ), + borderRadius: BorderRadius.circular(12.sp), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Image + ClipRRect( + borderRadius: BorderRadius.circular(8.sp), + child: offer.mobileBannerImage != null && + offer.mobileBannerImage!.isNotEmpty + ? Image.network( + '${ApiUrls.baseUrl}/${offer.mobileBannerImage}', + width: double.infinity, + height: 120.5.h, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: double.infinity, + height: 120.5.h, + color: const Color(0xFFFEE7E7), + child: Icon( + Icons.local_offer, + size: 40.sp, + color: + const Color(0xFFF95F62).withOpacity(.6), ), - ), - ); - }, - ) - : Container( - width: double.infinity, - height: 120.5.h, - color: const Color(0xFFFEE7E7), - child: Icon( - Icons.local_offer, - size: 40.sp, - color: - const Color(0xFFF95F62).withOpacity(.6), + ); + }, + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) return child; + + return Container( + width: double.infinity, + height: 120.5.h, + color: const Color(0xFFFEE7E7), + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + color: const Color(0xFFF95F62), + value: loadingProgress + .expectedTotalBytes != + null + ? loadingProgress + .cumulativeBytesLoaded / + loadingProgress + .expectedTotalBytes! + : null, + ), + ), + ); + }, + ) + : Container( + width: double.infinity, + height: 120.5.h, + color: const Color(0xFFFEE7E7), + child: Icon( + Icons.local_offer, + size: 40.sp, + color: + const Color(0xFFF95F62).withOpacity(.6), + ), ), ), - ), - SizedBox(height: 8.h), + SizedBox(height: 8.h), - /// Title - CustomText( - text: offer.title, - size: 18.sp, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + /// Title + CustomText( + text: offer.title, + size: 18.sp, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), - SizedBox(height: 8.h), + SizedBox(height: 8.h), - /// Offer Code - CustomText( - text: offer.description??"N/A", - color: Colors.black.withOpacity(.6), - size: 12.sp, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + /// Offer Code + CustomText( + text: offer.description??"N/A", + color: Colors.black.withOpacity(.6), + size: 12.sp, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ); }, @@ -386,35 +394,43 @@ class BuyPassContent extends StatelessWidget { color: Colors.grey[200], borderRadius: BorderRadius.circular(8.r), ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8.r), - child: attraction.thumbnail != null && - attraction.thumbnail!.isNotEmpty - ? Image.network( - attraction.thumbnail!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Icon( - Icons.location_on, - size: 40.sp, - color: Colors.grey[400], - ); - }, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Center( - child: SizedBox( - width: 20.w, - height: 20.w, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ); - }, - ) - : Icon( - Icons.location_on, - size: 40.sp, - color: Colors.grey[400], + child: GestureDetector( + onTap: () { + // Navigator.of(context).pushNamed( + // RouteConstants.attractionDetails, + // arguments: attraction, + // ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: attraction.thumbnail != null && + attraction.thumbnail!.isNotEmpty + ? Image.network( + attraction.thumbnail!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.location_on, + size: 40.sp, + color: Colors.grey[400], + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + }, + ) + : Icon( + Icons.location_on, + size: 40.sp, + color: Colors.grey[400], + ), ), ), ), @@ -447,12 +463,20 @@ class BuyPassContent extends StatelessWidget { ), SizedBox(height: 20.h), - Align( - alignment: Alignment.center, - child: CustomText( - text: "View All", - size: 12.sp, - color: Color(0xFFF95F62), + GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + RouteConstants.attractionsPage, + arguments: "home", + ); + }, + child: Align( + alignment: Alignment.center, + child: CustomText( + text: "View All", + size: 12.sp, + color: Color(0xFFF95F62), + ), ), ), SizedBox(height: 41.h), diff --git a/lib/buy_a_pass/widget/payment_card_view.dart b/lib/buy_a_pass/widget/payment_card_view.dart index f88b7f2..23d29c3 100644 --- a/lib/buy_a_pass/widget/payment_card_view.dart +++ b/lib/buy_a_pass/widget/payment_card_view.dart @@ -3,6 +3,7 @@ import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../localPreference/local_preference.dart'; import '../models/checkout_model.dart'; import '../../checkout/view/checkout_view.dart'; // ✅ Import CheckoutView directly @@ -127,7 +128,7 @@ class PaymentCard extends StatelessWidget { ), SizedBox(height: 20.h), CustomFilledButton( - onTap: () { + onTap: () async { // ✅ Create checkout data final checkoutData = CheckoutData( cityName: city, @@ -144,6 +145,21 @@ class PaymentCard extends StatelessWidget { description: description, ); + await LocalPreference.setPassCart( + cityName: city, + heroImage: heroImage, + cardTypeName: cardType, + cardDisplayName: cardDisplayName, + themeColor: themeColor.value, // Convert Color to int + adultCount: adults, + childCount: children, + adultPrice: adultPrice, + childPrice: childPrice, + validityDuration: selectedValue, + totalPrice: totalPrice, + description: description, + ); + // ✅ DIRECT NAVIGATION - This fixes the route issue! Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart b/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart new file mode 100644 index 0000000..67607d4 --- /dev/null +++ b/lib/cart/blocs/myPassCart/my_pass_cart_bloc.dart @@ -0,0 +1,73 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/my_pass_cart_repository.dart'; +import 'my_pass_cart_event.dart'; +import 'my_pass_cart_state.dart'; + +class MyPassCartBloc extends Bloc { + final MyPassCartRepository repository; + + MyPassCartBloc({required this.repository}) : super(const MyPassCartInitial()) { + on(_onFetchPassCart); + on(_onClearPassCart); + } + + /// Handle fetching pass cart data + Future _onFetchPassCart( + FetchPassCartEvent event, + Emitter emit, + ) async { + try { + if (kDebugMode) { + print('🔄 [BLOC] Fetching pass cart...'); + } + + emit(const MyPassCartLoading()); + + final cartData = await repository.fetchPassesCartByLocal(); + + if (cartData != null) { + if (kDebugMode) { + print('✅ [BLOC] Cart data loaded successfully'); + } + emit(MyPassCartLoaded(cartData: cartData)); + } else { + if (kDebugMode) { + print('â„šī¸ [BLOC] Cart is empty'); + } + emit(const MyPassCartEmpty()); + } + } catch (e) { + if (kDebugMode) { + print('❌ [BLOC] Error fetching cart: $e'); + } + emit(MyPassCartError(message: e.toString())); + } + } + + /// Handle clearing pass cart + Future _onClearPassCart( + ClearPassCartEvent event, + Emitter emit, + ) async { + try { + if (kDebugMode) { + print('🔄 [BLOC] Clearing pass cart...'); + } + + // You can add clearPassCart method to repository if needed + // await repository.clearPassCartFromLocal(); + + emit(const MyPassCartEmpty()); + + if (kDebugMode) { + print('✅ [BLOC] Cart cleared successfully'); + } + } catch (e) { + if (kDebugMode) { + print('❌ [BLOC] Error clearing cart: $e'); + } + emit(MyPassCartError(message: e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_event.dart b/lib/cart/blocs/myPassCart/my_pass_cart_event.dart new file mode 100644 index 0000000..5bd32ad --- /dev/null +++ b/lib/cart/blocs/myPassCart/my_pass_cart_event.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +abstract class MyPassCartEvent extends Equatable { + const MyPassCartEvent(); + + @override + List get props => []; +} + +/// Event to fetch pass cart data from local database +class FetchPassCartEvent extends MyPassCartEvent { + const FetchPassCartEvent(); +} + +/// Event to clear pass cart +class ClearPassCartEvent extends MyPassCartEvent { + const ClearPassCartEvent(); +} \ No newline at end of file diff --git a/lib/cart/blocs/myPassCart/my_pass_cart_state.dart b/lib/cart/blocs/myPassCart/my_pass_cart_state.dart new file mode 100644 index 0000000..3d6ea24 --- /dev/null +++ b/lib/cart/blocs/myPassCart/my_pass_cart_state.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; + +abstract class MyPassCartState extends Equatable { + const MyPassCartState(); + + @override + List get props => []; +} + +/// Initial state +class MyPassCartInitial extends MyPassCartState { + const MyPassCartInitial(); +} + +/// Loading state when fetching cart data +class MyPassCartLoading extends MyPassCartState { + const MyPassCartLoading(); +} + +/// Loaded state with cart data +class MyPassCartLoaded extends MyPassCartState { + final Map cartData; + + const MyPassCartLoaded({required this.cartData}); + + @override + List get props => [cartData]; +} + +/// Empty state when no cart data exists +class MyPassCartEmpty extends MyPassCartState { + const MyPassCartEmpty(); +} + +/// Error state +class MyPassCartError extends MyPassCartState { + final String message; + + const MyPassCartError({required this.message}); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/cart/repository/my_pass_cart_repository.dart b/lib/cart/repository/my_pass_cart_repository.dart new file mode 100644 index 0000000..c4b9be3 --- /dev/null +++ b/lib/cart/repository/my_pass_cart_repository.dart @@ -0,0 +1,35 @@ +import 'package:flutter/foundation.dart'; + +import '../../localPreference/local_preference.dart'; + +class MyPassCartRepository { + + /// Fetch pass cart data from local database + Future?> fetchPassesCartByLocal() async { + try { + if (kDebugMode) { + print('🔄 [REPO] Fetching pass cart from local database...'); + } + + final passCartData = await LocalPreference.getPassCart(); + + if (passCartData != null) { + if (kDebugMode) { + print('✅ [REPO] Pass cart retrieved successfully'); + print('đŸ“Ļ [REPO] Cart details: ${passCartData['card_display_name']} - ${passCartData['city_name']}'); + } + return passCartData; + } else { + if (kDebugMode) { + print('â„šī¸ [REPO] No pass cart data found in local database'); + } + return null; + } + } catch (e) { + if (kDebugMode) { + print('❌ [REPO] Error fetching pass cart: $e'); + } + rethrow; + } + } +} \ No newline at end of file diff --git a/lib/cart/views/my_cart_view_page.dart b/lib/cart/views/my_cart_view_page.dart index 4005474..c1cead0 100644 --- a/lib/cart/views/my_cart_view_page.dart +++ b/lib/cart/views/my_cart_view_page.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../common_packages/back_widget.dart'; -import '../blocs/pass_bloc.dart'; +import '../blocs/myPassCart/my_pass_cart_bloc.dart'; +import '../blocs/myPassCart/my_pass_cart_event.dart'; import '../blocs/postcard_bloc.dart'; -import 'my_pass_page_view.dart'; +import '../repository/my_pass_cart_repository.dart'; +import 'my_pass_cart_page_view.dart'; import 'my_postcard_page_view.dart'; class MyCartPage extends StatefulWidget { @@ -22,8 +24,14 @@ class _MyCartPageState extends State { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider(create: (_) => PassBloc()..add(LoadPasses())), - BlocProvider(create: (_) => PostCardBloc()..add(LoadPostCards())), + BlocProvider( + create: (_) => PostCardBloc()..add(LoadPostCards()), + ), + BlocProvider( + create: (_) => MyPassCartBloc( + repository: MyPassCartRepository(), + )..add(const FetchPassCartEvent()), + ), ], child: Scaffold( backgroundColor: Colors.white, diff --git a/lib/cart/views/my_pass_cart_page_view.dart b/lib/cart/views/my_pass_cart_page_view.dart new file mode 100644 index 0000000..6f8d349 --- /dev/null +++ b/lib/cart/views/my_pass_cart_page_view.dart @@ -0,0 +1,486 @@ +import 'package:citycards_customer/cart/views/view_pass_page_view.dart'; +import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart'; +import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; +import 'package:citycards_customer/common_packages/custom_filled_button.dart'; +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 '../../login/view/login_email_bottomsheet.dart'; +import '../../common_packages/common_app_texts.dart'; +import '../../localPreference/local_preference.dart'; +import '../blocs/myPassCart/my_pass_cart_bloc.dart'; +import '../blocs/myPassCart/my_pass_cart_event.dart'; +import '../blocs/myPassCart/my_pass_cart_state.dart'; + +class MyPassesPage extends StatefulWidget { + const MyPassesPage({super.key}); + + @override + State createState() => _MyPassesPageState(); +} + +class _MyPassesPageState extends State { + // For coupon/discount management + String? appliedCouponCode; + double discountPercentage = 0.0; + + @override + void initState() { + super.initState(); + // Fetch cart data when page loads + context.read().add(const FetchPassCartEvent()); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + 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?; + + // Calculate pricing + final double subtotal = totalPrice; + final double discountAmount = subtotal * (discountPercentage / 100); + final double taxRate = 0.05; // 5% tax + final double totalBeforeTax = subtotal - discountAmount; + final double taxAmount = totalBeforeTax * taxRate; + 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: [ + 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, + ), + ), + // TextSpan( + // text: "Card", + // style: TextStyle( + // color: Colors.white, + // fontSize: 12.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: "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(), + 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), + 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 (discountPercentage > 0) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomText(text: "Discount", size: 14.sp), + CustomText( + text: "-\$${discountAmount.toStringAsFixed(2)} (${discountPercentage.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 \$${taxAmount.toStringAsFixed(2)} in taxes", + size: 12.sp, + color: Colors.black.withOpacity(0.6), + ), + ], + ), + ), + CustomText( + text: "\$${finalTotal.toStringAsFixed(2)}", + size: 24.sp, + weight: FontWeight.w500, + ), + ], + ), + 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) { + return Center( + child: Column( + children: [ + Image.asset("assets/gif/empty_cart.gif", width: 250.w), + CustomText( + text: "You do not have any passes", + size: 24.sp, + color: Color(0xFFF95F62), + ), + SizedBox(height: 4.h), + Text( + "Get a pass and get offers and discounts and more on your trip to your favourite city", + style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp), + textAlign: TextAlign.center, + ), + ], + ), + ); + } else if (state is MyPassCartError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 60.sp, color: Colors.red), + SizedBox(height: 16.h), + CustomText( + text: "Error loading cart", + size: 16.sp, + color: Colors.red, + ), + SizedBox(height: 8.h), + CustomText( + text: state.message, + size: 12.sp, + color: Colors.grey, + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + }, + ); + } +} \ No newline at end of file diff --git a/lib/cart/views/my_pass_page_view.dart b/lib/cart/views/my_pass_page_view.dart deleted file mode 100644 index ac3bf04..0000000 --- a/lib/cart/views/my_pass_page_view.dart +++ /dev/null @@ -1,379 +0,0 @@ -import 'package:citycards_customer/cart/views/view_pass_page_view.dart'; -import 'package:citycards_customer/checkout/widget/all_coupons_bottomsheet.dart'; -import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; -import 'package:citycards_customer/common_packages/custom_filled_button.dart'; -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 '../../login/view/login_email_bottomsheet.dart'; -import '../../common_packages/common_app_texts.dart'; -import '../blocs/pass_bloc.dart'; - -class MyPassesPage extends StatelessWidget { - const MyPassesPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is PassLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is PassLoaded) { - return - Column( - children: [ - SizedBox(height: 22.h), - Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: Color(0xFFF95FAF).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: 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: "Melbourne", - weight: FontWeight.w500, - size: 16.sp, - ), - SizedBox(height: 5.h), - CustomText( - text: "2 Days", - 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: [ - Image.asset( - 'assets/icons/adult.png', - scale: 4, - ), - SizedBox(width: 4.w), - CustomText( - text: "3 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: " 2", - 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: "3 Kids", - color: Color(0xFF8E8E8E), - size: 12.sp, - ), - - SizedBox(width: 53.w), - - CustomText( - text: "\$49.50", - size: 24.sp, - weight: FontWeight.w500, - color: Color(0xFFF95F62), - ), - ], - ), - ], - ), - ], - ), - - Container( - width: 35.w, - height: 123.h, - decoration: BoxDecoration( - color: Color(0xFFF97316), - 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: "${CommonAppText.selectiveCard} ", - style: TextStyle( - color: Colors.white, - fontSize: 16.sp, - ), - ), - TextSpan( - text: "Card", - style: TextStyle( - color: Colors.white, - fontSize: 12.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: "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(), - 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: "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), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomText(text: "Subtotal", size: 14.sp), - CustomText( - text: "\$49.50", - size: 14.sp, - weight: FontWeight.w500, - ), - ], - ), - SizedBox(height: 14.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomText(text: "Discount", size: 14.sp), - CustomText( - text: "-7.20%", - size: 14.sp, - weight: FontWeight.w500, - ), - ], - ), - SizedBox(height: 10.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 \$2.24 in taxes", - size: 12.sp, - color: Colors.black.withOpacity(0.6), - ), - ], - ), - ), - CustomText( - text: "\$42.60", - size: 24.sp, - weight: FontWeight.w500, - ), - ], - ), - - SizedBox(height: 150.h,), - - CustomFilledButton( - onTap: () { - 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: "Login to Checkout", - ), - SizedBox(height: 25.h), - ], - ); - } - return Center( - child: Column( - children: [ - Image.asset("assets/gif/empty_cart.gif", width: 250.w), - CustomText( - text: "You do not have any passes", - size: 24.sp, - color: Color(0xFFF95F62), - ), - SizedBox(height: 4.h), - Text( - "Get a pass and get offers and discounts and more on your trip to your favourite city", - style: TextStyle(color: Color(0xFF656565), fontSize: 14.sp), - textAlign: TextAlign.center, - ), - ], - ), - ); - }, - ); - } -} diff --git a/lib/postcard/models/postcard_model.dart b/lib/checkout/bloc/allCoupons/all_coupons_bloc.dart similarity index 100% rename from lib/postcard/models/postcard_model.dart rename to lib/checkout/bloc/allCoupons/all_coupons_bloc.dart diff --git a/lib/postcard/repository/postcard_repository.dart b/lib/checkout/bloc/allCoupons/all_coupons_event.dart similarity index 100% rename from lib/postcard/repository/postcard_repository.dart rename to lib/checkout/bloc/allCoupons/all_coupons_event.dart diff --git a/lib/checkout/bloc/allCoupons/all_coupons_state.dart b/lib/checkout/bloc/allCoupons/all_coupons_state.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/checkout/bloc/checkout/checkout_bloc.dart b/lib/checkout/bloc/checkout/checkout_bloc.dart deleted file mode 100644 index 4cae026..0000000 --- a/lib/checkout/bloc/checkout/checkout_bloc.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; - -part 'checkout_event.dart'; -part 'checkout_state.dart'; - -/// BLoC for managing checkout screen state -class CheckoutBloc extends Bloc { - CheckoutBloc() : super(CheckoutState.initial()) { - // Handle apply coupon event - on(_onApplyCoupon); - - // Handle remove coupon event - on(_onRemoveCoupon); - - // Handle confirm purchase details event - on(_onConfirmPurchaseDetails); - - // Handle reset purchase details event - on(_onResetPurchaseDetails); - } - - /// Handle applying a coupon - void _onApplyCoupon(ApplyCouponEvent event, Emitter emit) { - emit(state.copyWith( - appliedCouponCode: event.couponCode, - discountPercentage: event.discountPercentage, - )); - } - - /// Handle removing a coupon - void _onRemoveCoupon(RemoveCouponEvent event, Emitter emit) { - emit(state.copyWith( - clearCoupon: true, - discountPercentage: 0.0, - )); - } - - /// Handle confirming purchase details - void _onConfirmPurchaseDetails( - ConfirmPurchaseDetailsEvent event, - Emitter emit, - ) { - emit(state.copyWith(isPurchaseDetailsConfirmed: true)); - } - - /// Handle resetting purchase details confirmation - void _onResetPurchaseDetails( - ResetPurchaseDetailsEvent event, - Emitter emit, - ) { - emit(state.copyWith(isPurchaseDetailsConfirmed: false)); - } -} \ No newline at end of file diff --git a/lib/checkout/bloc/checkout/checkout_event.dart b/lib/checkout/bloc/checkout/checkout_event.dart deleted file mode 100644 index 7ed9c86..0000000 --- a/lib/checkout/bloc/checkout/checkout_event.dart +++ /dev/null @@ -1,24 +0,0 @@ -part of 'checkout_bloc.dart'; - -/// Base class for all checkout events -abstract class CheckoutEvent {} - -/// Event to apply a coupon code -class ApplyCouponEvent extends CheckoutEvent { - final String couponCode; - final double discountPercentage; - - ApplyCouponEvent({ - required this.couponCode, - required this.discountPercentage, - }); -} - -/// Event to remove the applied coupon -class RemoveCouponEvent extends CheckoutEvent {} - -/// Event to confirm purchase details -class ConfirmPurchaseDetailsEvent extends CheckoutEvent {} - -/// Event to reset purchase details confirmation -class ResetPurchaseDetailsEvent extends CheckoutEvent {} \ No newline at end of file diff --git a/lib/checkout/bloc/checkout/checkout_state.dart b/lib/checkout/bloc/checkout/checkout_state.dart deleted file mode 100644 index 00db8c2..0000000 --- a/lib/checkout/bloc/checkout/checkout_state.dart +++ /dev/null @@ -1,52 +0,0 @@ -part of 'checkout_bloc.dart'; - -/// State class for checkout screen -class CheckoutState { - final String? appliedCouponCode; - final double discountPercentage; - final bool isPurchaseDetailsConfirmed; - - const CheckoutState({ - this.appliedCouponCode, - this.discountPercentage = 0.0, - this.isPurchaseDetailsConfirmed = false, - }); - - /// Initial state - factory CheckoutState.initial() { - return const CheckoutState(); - } - - /// Copy with method for immutable state updates - CheckoutState copyWith({ - String? appliedCouponCode, - double? discountPercentage, - bool? isPurchaseDetailsConfirmed, - bool clearCoupon = false, - }) { - return CheckoutState( - appliedCouponCode: clearCoupon ? null : appliedCouponCode ?? this.appliedCouponCode, - discountPercentage: discountPercentage ?? this.discountPercentage, - isPurchaseDetailsConfirmed: isPurchaseDetailsConfirmed ?? this.isPurchaseDetailsConfirmed, - ); - } - - /// Calculate discount amount based on subtotal - double calculateDiscountAmount(double subtotal) { - return subtotal * (discountPercentage / 100); - } - - /// Calculate tax amount - double calculateTaxAmount(double subtotal, {double taxRate = 0.05}) { - final totalBeforeTax = subtotal - calculateDiscountAmount(subtotal); - return totalBeforeTax * taxRate; - } - - /// Calculate final total - double calculateFinalTotal(double subtotal, {double taxRate = 0.05}) { - final discountAmount = calculateDiscountAmount(subtotal); - final totalBeforeTax = subtotal - discountAmount; - final taxAmount = totalBeforeTax * taxRate; - return totalBeforeTax + taxAmount; - } -} \ No newline at end of file diff --git a/lib/checkout/models/all_coupons_model.dart b/lib/checkout/models/all_coupons_model.dart new file mode 100644 index 0000000..60698de --- /dev/null +++ b/lib/checkout/models/all_coupons_model.dart @@ -0,0 +1,61 @@ +class AllCouponsModel { + final int id; + final String title; + final String? description; + final int cityXid; + final int discountPercent; + final String couponCode; + final DateTime startDateTime; + final DateTime endDateTime; + final bool showAtCheckout; + final String couponStatus; + final bool isActive; + + AllCouponsModel({ + required this.id, + required this.title, + this.description, + required this.cityXid, + required this.discountPercent, + required this.couponCode, + required this.startDateTime, + required this.endDateTime, + required this.showAtCheckout, + required this.couponStatus, + required this.isActive, + }); + + /// From JSON + factory AllCouponsModel.fromJson(Map json) { + return AllCouponsModel( + id: json['id'] as int, + title: json['title'] as String, + description: json['description'], + cityXid: json['cityXid'] as int, + discountPercent: json['discountPercent'] as int, + couponCode: json['couponCode'] as String, + startDateTime: DateTime.parse(json['startDateTime']), + endDateTime: DateTime.parse(json['endDateTime']), + showAtCheckout: json['showAtCheckout'] as bool, + couponStatus: json['couponStatus'] as String, + isActive: json['isActive'] as bool, + ); + } + + /// To JSON + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'cityXid': cityXid, + 'discountPercent': discountPercent, + 'couponCode': couponCode, + 'startDateTime': startDateTime.toIso8601String(), + 'endDateTime': endDateTime.toIso8601String(), + 'showAtCheckout': showAtCheckout, + 'couponStatus': couponStatus, + 'isActive': isActive, + }; + } +} diff --git a/lib/checkout/repository/all_coupons_repository.dart b/lib/checkout/repository/all_coupons_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/checkout/view/checkout_view.dart b/lib/checkout/view/checkout_view.dart index 8813992..83e18be 100644 --- a/lib/checkout/view/checkout_view.dart +++ b/lib/checkout/view/checkout_view.dart @@ -5,15 +5,10 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_dashed_line.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; - import '../../StripePayment/view/stripe_payment.dart'; import '../../buy_a_pass/models/checkout_model.dart'; -import '../../common_packages/common_app_texts.dart'; import '../../localPreference/local_preference.dart'; -import '../../postcard/widgets/purchase_details_bottom_sheet.dart'; -import '../bloc/pass_purchase_details_bloc.dart'; import '../widget/pass_purchase_details_bottomsheet.dart'; class CheckoutView extends StatefulWidget { diff --git a/lib/common_packages/app_bar.dart b/lib/common_packages/app_bar.dart index 6180a3c..b53bbcc 100644 --- a/lib/common_packages/app_bar.dart +++ b/lib/common_packages/app_bar.dart @@ -1,8 +1,13 @@ +import 'package:citycards_customer/networkApiServices/api_urls.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../core/route_constants.dart'; import '../home/widgets/search_city_bottomsheet.dart'; +import '../localPreference/local_preference.dart'; +import '../profile/bloc/profile/profile_bloc.dart'; +import '../profile/bloc/profile/profile_state.dart'; class CommonAppBar extends StatelessWidget { const CommonAppBar({ @@ -115,11 +120,39 @@ class CommonAppBar extends StatelessWidget { rootNavigator: true, ).pushNamed(RouteConstants.profile); }, - child: CircleAvatar( - backgroundColor: const Color(0xffFFDFDF), - child: Image.asset( - "assets/images/profile_default_img.png", - ), + child: BlocBuilder( + builder: (context, state) { + String? imagePath; + + // ✅ Get image from profile state + if (state is ProfileLoaded) { + imagePath = state.profile.profileImage; + } + + // ✅ Build full image URL + final String? imageUrl = + (imagePath != null && imagePath.isNotEmpty) + ? "${ApiUrls.baseUrl}$imagePath" + : null; + + return CircleAvatar( + radius: 20.r, + backgroundColor: const Color(0xffFFDFDF), + + // ✅ Network image only if exists + backgroundImage: + (imageUrl != null && imageUrl.isNotEmpty) + ? NetworkImage(imageUrl) + : null, + + // ✅ Default fallback (unchanged) + child: (imageUrl == null || imageUrl.isEmpty) + ? Image.asset( + "assets/images/profile_default_img.png", + ) + : null, + ); + }, ), ), ], diff --git a/lib/contact_us/contact_us_view.dart b/lib/contact_us/contact_us_view.dart deleted file mode 100644 index ad56f7c..0000000 --- a/lib/contact_us/contact_us_view.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:citycards_customer/common_packages/back_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:citycards_customer/common_packages/custom_textfield.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class ContactUsPage extends StatelessWidget { - const ContactUsPage({super.key}); - - @override - Widget build(BuildContext context) { - final TextEditingController firstNameController = TextEditingController(); - final TextEditingController lastNameController = TextEditingController(); - final TextEditingController emailController = TextEditingController(); - final TextEditingController phoneController = TextEditingController(); - final TextEditingController messageController = TextEditingController(); - - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header bar - CommonAppBar(isWhiteLogo: false, isProfilePage: true, showDivider: true,), - - backWidget(context,"Contact Us", Colors.black), - SizedBox(height: 22.h), - - CustomText( - text: - "You can get in touch with us through the below platforms. Our team will contact you shortly", - size: 14.sp, - color: Colors.black.withOpacity(.6), - ), - SizedBox(height: 20.h), - - // Customer Support Section - Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 16.h), - decoration: BoxDecoration( - color: Color(0x00000005).withOpacity(.02), - borderRadius: BorderRadius.circular(12.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText( - text: "Customer Support", - size: 18.sp, - weight: FontWeight.w500, - ), - SizedBox(height: 16.h), - _supportBox( - icon: Icons.phone, - title: "Contact Number", - subtitle: "+1012 3456 789", - action: "Tap to call", - ), - SizedBox(height: 12.h), - _supportBox( - icon: Icons.email_rounded, - title: "Email", - subtitle: "citycards24@gmail.com", - action: "Tap to email", - ), - SizedBox(height: 12.h), - _supportBox( - icon: Icons.location_on, - title: "Location", - subtitle: - "132 Dartmouth Street Boston, Massachusetts 02156 United States", - action: "View on map", - ), - ], - ), - ), - SizedBox(height: 24.h), - - // Text fields - CustomTextField( - label: "First Name", - hint: "Enter your first name", - controller: firstNameController, - ), - CustomTextField( - label: "Last Name", - hint: "Enter your last name", - controller: lastNameController, - ), - CustomTextField( - label: "Email", - hint: "Enter your email address", - controller: emailController, - ), - CustomTextField( - label: "Phone Number", - hint: "Enter your phone number", - controller: phoneController, - ), - - CustomTextField( - label: "Description", - hint: "Write your message here", - maxLines: 4, - controller: messageController, - ), - - // _descriptionField(messageController), - SizedBox(height: 24.h), - - // Submit Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF95F62), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(38.r), - ), - padding: EdgeInsets.symmetric(vertical: 6.h), - ), - onPressed: () {}, - child: CustomText( - text: "Submit Ticket", - size: 16.sp, - weight: FontWeight.w500, - color: Colors.white, - ), - ), - ), - SizedBox(height: 20.h), - ], - ), - ), - ), - ); - } - - // --- Support Info Box --- - Widget _supportBox({ - required IconData icon, - required String title, - required String subtitle, - required String action, - }) { - return Container( - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: const Color(0xFFF95F62), width: 0.8), - color: Colors.white, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(icon, color: const Color(0xFFF95F62), size: 32.sp), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText( - text: title, - size: 11.sp, - weight: FontWeight.w600, - color: Color(0x00000000).withOpacity(.6), - ), - SizedBox(height: 6.h), - Text( - subtitle, - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - ), - SizedBox(height: 2.h), - Text( - action, - style: TextStyle( - fontSize: 11.sp, - color: Color(0xFF000000).withOpacity(.4), - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ), - ], - ), - ); - } - - // --- Description Field --- - Widget _descriptionField(TextEditingController controller) { - return Padding( - padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText(text: "Description", size: 14.sp), - SizedBox(height: 6.h), - TextField( - controller: controller, - maxLines: 4, - decoration: InputDecoration( - hintText: "Write your message here", - hintStyle: TextStyle(fontSize: 12.sp, color: Color(0xFF8E8E8E)), - filled: true, - fillColor: const Color(0xFFFFF5F5), - contentPadding: EdgeInsets.symmetric( - horizontal: 24.w, - vertical: 12.h, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: .4.w, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide(color: Color(0xFFF95F62), width: 1.w), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index f733105..47812b7 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -5,7 +5,6 @@ import 'package:citycards_customer/attractions/models/attraction_model.dart'; import 'package:citycards_customer/buy_a_pass/view/buy_pass_view.dart'; import 'package:citycards_customer/checkout/view/checkout_view.dart'; import 'package:citycards_customer/common_bloc/language_selection_bloc.dart'; -import 'package:citycards_customer/contact_us/contact_us_view.dart'; import 'package:citycards_customer/create_account/view/create_account_view.dart'; import 'package:citycards_customer/esim_offer/esim_offer_view.dart'; import 'package:citycards_customer/hotel_offer/hotel_offer_view.dart'; @@ -29,6 +28,7 @@ import '../cart/views/my_cart_view_page.dart'; import '../common_bloc/bottom_navigation_bloc.dart'; import '../home/views/home_page_view.dart'; import '../home/views/registered_user_home_page.dart'; +import '../profile/view/contact_us/contact_us_view.dart'; import '../profile/view/edit_profile/edit_profile_view.dart'; import '../profile/view/faq/faq_view.dart'; import '../profile/view/privacy/privacy_view.dart'; diff --git a/lib/create_account/bloc/create_account_bloc.dart b/lib/create_account/bloc/create_account_bloc.dart index 6c2abcd..cd62f97 100644 --- a/lib/create_account/bloc/create_account_bloc.dart +++ b/lib/create_account/bloc/create_account_bloc.dart @@ -45,6 +45,7 @@ class CreateAccountBloc extends Bloc { role: userModel.user.role, roleId: userModel.user.roleId, ); + await LocalPreference.setProfileImage(userModel.user.profileImage); emit(CreateAccountSuccess( message: response['message'] ?? 'Account created successfully', userData: response['data'] ?? {}, diff --git a/lib/create_account/models/create_account_model.dart b/lib/create_account/models/create_account_model.dart index 506d46e..7f28757 100644 --- a/lib/create_account/models/create_account_model.dart +++ b/lib/create_account/models/create_account_model.dart @@ -47,6 +47,7 @@ class User { final String lastName; final String fullName; final String emailAddress; + final String profileImage; // ✅ newly added final String role; final int roleId; @@ -56,6 +57,7 @@ class User { required this.lastName, required this.fullName, required this.emailAddress, + required this.profileImage, required this.role, required this.roleId, }); @@ -67,6 +69,7 @@ class User { lastName: json['lastName'] ?? '', fullName: json['fullName'] ?? '', emailAddress: json['emailAddress'] ?? '', + profileImage: json['profileImage'] ?? '', role: json['role'] ?? '', roleId: json['roleId'] ?? 0, ); @@ -79,6 +82,7 @@ class User { 'lastName': lastName, 'fullName': fullName, 'emailAddress': emailAddress, + 'profileImage': profileImage, 'role': role, 'roleId': roleId, }; diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart index 3fa9f15..a0d3c5e 100644 --- a/lib/create_account/view/create_account_view.dart +++ b/lib/create_account/view/create_account_view.dart @@ -5,6 +5,9 @@ import 'package:citycards_customer/common_packages/custom_textfield.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../localPreference/local_preference.dart'; +import '../../profile/bloc/profile/profile_bloc.dart'; +import '../../profile/bloc/profile/profile_event.dart'; import '../bloc/create_account_bloc.dart'; import '../bloc/create_account_event.dart'; import '../bloc/create_account_state.dart'; @@ -52,11 +55,15 @@ class CreateAccountView extends StatelessWidget { repository: CreateAccountRepository(), ), child: BlocListener( - listener: (context, state) { + listener: (context, state) async { if (state is CreateAccountSuccess) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.message)), ); + await LocalPreference.setLogin(true); + final userId = await LocalPreference.getUserId(); + context.read().add(FetchProfileEvent(userId: userId!)); + context.read().add(CheckLoginStatusEvent()); Navigator.pop(context); Navigator.pop(context); } else if (state is CreateAccountFailure) { diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index af9859b..024b4a6 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -201,13 +201,23 @@ class _FirstTimeUserHomePageState extends State { // Determine if it's a network image or asset final isNetworkImage = imageUrl.startsWith('http'); - return ExploreCitiesCard( - name: city.cityName ?? 'N/A', - description: city.tagLine ?? 'N/A', - imageUrl: imageUrl, - individualPrice: '\$${city.indivisualTicketAmt ?? 0}+', - cityCardPrice: '\$${city.cityCardTicketAmt ?? 0}', - savingsText: city.saveLabel ?? 'Save \$0+', + return GestureDetector( + onTap: () async { + await LocalPreference.updateOnboardingPage(2); + await LocalPreference.setSelectedCityId(city.id!); + Navigator.pushReplacementNamed( + context, + RouteConstants.home, + ); + }, + child: ExploreCitiesCard( + name: city.cityName ?? 'N/A', + description: city.tagLine ?? 'N/A', + imageUrl: imageUrl, + individualPrice: '\$${city.indivisualTicketAmt ?? 0}+', + cityCardPrice: '\$${city.cityCardTicketAmt ?? 0}', + savingsText: city.saveLabel ?? 'Save \$0+', + ), ); }, ), diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index 07659a1..a7e4201 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -1,4 +1,5 @@ import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart'; +import 'package:citycards_customer/postcard/views/my_postcards_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:citycards_customer/common_packages/custom_bottom_navbar.dart'; @@ -52,7 +53,7 @@ class _HomePageState extends State { buildOffstageNavigator( 3, currentIndex, - const PostcardPage(), + const MyPostCardsView(), _navigatorKeys[3], ), ], diff --git a/lib/home/views/registered_user_home_page.dart b/lib/home/views/registered_user_home_page.dart index 3521586..890fc2a 100644 --- a/lib/home/views/registered_user_home_page.dart +++ b/lib/home/views/registered_user_home_page.dart @@ -9,6 +9,10 @@ import '../../common_packages/app_bar.dart'; import '../../core/route_constants.dart'; import '../../localPreference/local_preference.dart'; import '../../networkApiServices/api_urls.dart'; +import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart'; +import '../../postcard/blocs/myPostCards/my_postcard_event.dart'; +import '../../profile/bloc/profile/profile_bloc.dart'; +import '../../profile/bloc/profile/profile_event.dart'; import '../bloc/registeredHome/home_bloc.dart'; import '../bloc/registeredHome/home_event.dart'; import '../bloc/registeredHome/home_state.dart'; @@ -31,7 +35,29 @@ class _RegisteredUserHomePageState extends State { @override void initState() { super.initState(); + // _loadMyPostCards(); _checkAndShowCitySelection(); + _loadProfileIfLoggedIn(); + } + Future _loadProfileIfLoggedIn() async { + final userId = await LocalPreference.getUserId(); + + if (userId != null && mounted) { + context.read().add( + FetchProfileEvent(userId: userId), + ); + } + } + + Future _loadMyPostCards() async { + final userId = await LocalPreference.getUserId(); + + if (userId != null && mounted) { + context.read().add(FetchDraftPostCards()); + context.read().add(RefreshDraftPostCards()); + context.read().add(RefreshOrderPostCards()); + context.read().add(FetchOrderPostCards()); + } } Future _checkAndShowCitySelection() async { diff --git a/lib/intro_screens/views/intro_screen_view.dart b/lib/intro_screens/views/intro_screen_view.dart index c6c8158..84c8cc2 100644 --- a/lib/intro_screens/views/intro_screen_view.dart +++ b/lib/intro_screens/views/intro_screen_view.dart @@ -79,7 +79,12 @@ class _IntroScreensViewState extends State { right: 20, child: GestureDetector( onTap: (){ - Navigator.pushReplacementNamed(context,RouteConstants.home); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const FirstTimeUserHomePage(), + ), + ); }, child: Container( height: 48.h, diff --git a/lib/localPreference/local_database.dart b/lib/localPreference/local_database.dart index 764a184..4380d0f 100644 --- a/lib/localPreference/local_database.dart +++ b/lib/localPreference/local_database.dart @@ -67,10 +67,29 @@ class LocalDatabase { full_name TEXT NOT NULL, email_address TEXT NOT NULL, role TEXT NOT NULL, - role_id INTEGER NOT NULL + role_id INTEGER NOT NULL, + profile_image TEXT ) '''); + /// PASS CART TABLE + await db.execute(''' + CREATE TABLE pass_cart ( + id INTEGER PRIMARY KEY, + city_name TEXT NOT NULL, + hero_image TEXT NOT NULL, + card_type_name TEXT NOT NULL, + card_display_name TEXT NOT NULL, + theme_color INTEGER NOT NULL, + adult_count INTEGER NOT NULL, + child_count INTEGER NOT NULL, + adult_price REAL NOT NULL, + child_price REAL NOT NULL, + validity_duration INTEGER NOT NULL, + total_price REAL NOT NULL, + description TEXT + ) +'''); }, ); diff --git a/lib/localPreference/local_preference.dart b/lib/localPreference/local_preference.dart index 0414915..2e20eba 100644 --- a/lib/localPreference/local_preference.dart +++ b/lib/localPreference/local_preference.dart @@ -1,4 +1,5 @@ import 'package:sqflite/sqflite.dart'; +import 'package:flutter/foundation.dart'; import 'local_database.dart'; class LocalPreference { @@ -121,6 +122,18 @@ class LocalPreference { return false; } + static Future clearLogin() async { + final db = await LocalDatabase().database; + + await db.update( + 'login_state', + {'is_logged_in': 0}, + where: 'id = ?', + whereArgs: [1], + ); + } + + /// Set user tokens static Future setTokens({ required String accessToken, @@ -205,6 +218,7 @@ class LocalPreference { required String emailAddress, required String role, required int roleId, + String? profileImage, // Added optional profileImage parameter }) async { final db = await LocalDatabase().database; @@ -219,6 +233,7 @@ class LocalPreference { 'email_address': emailAddress, 'role': role, 'role_id': roleId, + 'profile_image': profileImage, // Include profile image }, conflictAlgorithm: ConflictAlgorithm.replace, ); @@ -240,5 +255,218 @@ class LocalPreference { return null; } + /// Set profile image with error handling + static Future setProfileImage(String imageUrl) async { + try { + final db = await LocalDatabase().database; + + final result = await db.update( + 'user_details', + {'profile_image': imageUrl}, + where: 'id = ?', + whereArgs: [1], + ); + + if (kDebugMode) { + print('✅ [LOCAL_PREF] Profile image saved: $imageUrl'); + print('📊 [LOCAL_PREF] Rows affected: $result'); + } + } catch (e) { + if (kDebugMode) { + print('❌ [LOCAL_PREF] Error saving profile image: $e'); + } + rethrow; + } + } + + /// Get profile image + static Future getProfileImage() async { + try { + final db = await LocalDatabase().database; + + final result = await db.query( + 'user_details', + columns: ['profile_image'], + where: 'id = ?', + whereArgs: [1], + ); + + if (result.isNotEmpty) { + final imageUrl = result.first['profile_image'] as String?; + if (kDebugMode && imageUrl != null) { + print('✅ [LOCAL_PREF] Retrieved profile image: $imageUrl'); + } + return imageUrl; + } + return null; + } catch (e) { + if (kDebugMode) { + print('❌ [LOCAL_PREF] Error getting profile image: $e'); + } + return null; + } + } + + /// Set pass cart data + static Future setPassCart({ + required String cityName, + required String heroImage, + required String cardTypeName, + required String cardDisplayName, + required int themeColor, + required int adultCount, + required int childCount, + required double adultPrice, + required double childPrice, + required int validityDuration, + required double totalPrice, + String? description, + }) async { + final db = await LocalDatabase().database; + + await db.insert( + 'pass_cart', + { + 'id': 1, + 'city_name': cityName, + 'hero_image': heroImage, + 'card_type_name': cardTypeName, + 'card_display_name': cardDisplayName, + 'theme_color': themeColor, + 'adult_count': adultCount, + 'child_count': childCount, + 'adult_price': adultPrice, + 'child_price': childPrice, + 'validity_duration': validityDuration, + 'total_price': totalPrice, + 'description': description, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + + if (kDebugMode) { + print('✅ [LOCAL_PREF] Pass cart saved: $cardDisplayName for $cityName'); + } + } + + /// Get pass cart data + static Future?> getPassCart() async { + try { + final db = await LocalDatabase().database; + + final result = await db.query( + 'pass_cart', + where: 'id = ?', + whereArgs: [1], + ); + + if (result.isNotEmpty) { + if (kDebugMode) { + print('✅ [LOCAL_PREF] Retrieved pass cart data'); + } + return result.first; + } + return null; + } catch (e) { + if (kDebugMode) { + print('❌ [LOCAL_PREF] Error getting pass cart: $e'); + } + return null; + } + } + + static Future clearPassCart() async { + try { + final db = await LocalDatabase().database; + + await db.delete( + 'pass_cart', + where: 'id = ?', + whereArgs: [1], + ); + + if (kDebugMode) { + print('✅ [LOCAL_PREF] Pass cart cleared'); + } + } catch (e) { + if (kDebugMode) { + print('❌ [LOCAL_PREF] Error clearing pass cart: $e'); + } + } + } + + static Future clearUserDetails() async { + final db = await LocalDatabase().database; + + await db.update( + 'user_details', + { + 'user_id': null, + 'first_name': '', + 'last_name': '', + 'full_name': '', + 'email_address': '', + 'role': '', + 'role_id': 0, + 'profile_image': null, + }, + where: 'id = ?', + whereArgs: [1], + ); + } + + + static Future clearProfileImage() async { + try { + final db = await LocalDatabase().database; + + final result = await db.update( + 'user_details', + {'profile_image': null}, + where: 'id = ?', + whereArgs: [1], + ); + + if (kDebugMode) { + print('🧹 [LOCAL_PREF] Profile image cleared'); + print('📊 [LOCAL_PREF] Rows affected: $result'); + } + } catch (e) { + if (kDebugMode) { + print('❌ [LOCAL_PREF] Error clearing profile image: $e'); + } + rethrow; + } + } + + static Future resetAppData() async { + await clearLogin(); + await clearTokens(); + await clearUserDetails(); + await clearPassCart();// optional + await clearProfileImage();// optional + } + + static Future clearAllData() async { + try { + final db = await LocalDatabase().database; + + // Clear all tables + await db.delete('selected_city'); + await db.delete('login_state'); + await db.delete('user_tokens'); + await db.delete('user_details'); + await db.delete('pass_cart'); + + if (kDebugMode) { + print('🧹 [LOCAL_PREF] All local data cleared successfully'); + } + } catch (e) { + if (kDebugMode) { + print('❌ [LOCAL_PREF] Error clearing all local data: $e'); + } + rethrow; + } + } } \ No newline at end of file diff --git a/lib/login/bloc/verify/verify_bloc.dart b/lib/login/bloc/verify/verify_bloc.dart index 07b3378..0142249 100644 --- a/lib/login/bloc/verify/verify_bloc.dart +++ b/lib/login/bloc/verify/verify_bloc.dart @@ -41,6 +41,7 @@ class VerifyOtpBloc extends Bloc { role: userModel.user.role, roleId: userModel.user.roleId, ); + await LocalPreference.setProfileImage(userModel.user.profileImage); emit(VerifyOtpSuccess(response: userModel)); } catch (e) { emit(VerifyOtpError(errorMessage: e.toString())); diff --git a/lib/login/view/verify_otp_bottomsheet.dart b/lib/login/view/verify_otp_bottomsheet.dart index 595b01d..3c8146d 100644 --- a/lib/login/view/verify_otp_bottomsheet.dart +++ b/lib/login/view/verify_otp_bottomsheet.dart @@ -1,5 +1,6 @@ import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/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'; import 'package:flutter/material.dart'; @@ -8,6 +9,7 @@ import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../core/route_constants.dart'; import '../../localPreference/local_preference.dart'; +import '../../postcard/blocs/myPostCards/my_postcard_event.dart'; import '../bloc/verify/verify_bloc.dart'; import '../bloc/verify/verify_event.dart'; import '../bloc/verify/verify_state.dart'; @@ -39,6 +41,11 @@ class _VerifyOtpBottomsheetState extends State { final userId = await LocalPreference.getUserId(); context.read().add(FetchProfileEvent(userId: userId!)); context.read().add(CheckLoginStatusEvent()); + context.read().add(CheckLoginStatus()); + context.read().add(FetchDraftPostCards()); + context.read().add(RefreshDraftPostCards()); + context.read().add(RefreshOrderPostCards()); + context.read().add(FetchOrderPostCards()); // User exists - navigate to home/dashboard // Navigator.of(context).pushReplacementNamed(RouteConstants.home); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/main.dart b/lib/main.dart index 529fe33..0422f2e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,8 @@ import 'home/repository/home_repository.dart'; import 'login/bloc/login/login_bloc.dart'; import 'login/repository/login_repository.dart'; import 'my_pass/blocs/my_pass_bloc.dart'; +import 'postcard/blocs/myPostCards/my_postcard_bloc.dart'; +import 'postcard/repository/my_postcard_repository.dart'; import 'profile/bloc/profile/profile_bloc.dart'; import 'search_offers/repository/offers_repository.dart'; import 'search_offers/view/search_offers_with_listing.dart'; @@ -74,6 +76,11 @@ class MyApp extends StatelessWidget { child: const OffersScreen(), ), BlocProvider(create: (context) => ProfileBloc()), + BlocProvider( + create: (context) => MyPostCardBloc( + repository: MyPostCardsRepository(), + ), + ), ], child: MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 07c1262..3e10e99 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -15,10 +15,13 @@ class ApiUrls { static const offers = "$baseUrl/mobile/list/offers"; static const buyAPass = "$baseUrl/mobile/pass"; static const offersDetails = "$baseUrl/mobile/list/offers"; + static const myPostCards = "$baseUrl/mobile/postcards/all"; //Post Apis static const createAccount = "$baseUrl/mobile/user/register"; static const sendOtp = "$baseUrl/mobile/send-otp"; static const verifyOtp = "$baseUrl/mobile/user/verify-otp"; + static const submitTicket = "$baseUrl/mobile/user/support"; + static const createPostCard = "$baseUrl/mobile/postcards"; } \ No newline at end of file diff --git a/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart b/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart new file mode 100644 index 0000000..fa9b4d5 --- /dev/null +++ b/lib/postcard/blocs/myPostCards/my_postcard_bloc.dart @@ -0,0 +1,201 @@ +import 'package:citycards_customer/localPreference/local_preference.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'dart:developer' as developer; +import '../../repository/my_postcard_repository.dart'; +import 'my_postcard_event.dart'; +import 'my_postcard_state.dart'; + +class MyPostCardBloc extends Bloc { + final MyPostCardsRepository repository; + + MyPostCardBloc({required this.repository}) : super(const MyPostCardInitial()) { + on(_onCheckLoginStatus); + on(_onFetchDraftPostCards); + on(_onFetchOrderPostCards); + on(_onRefreshDraftPostCards); + on(_onRefreshOrderPostCards); + } + + /// Handle checking login status + Future _onCheckLoginStatus( + CheckLoginStatus event, + Emitter emit, + ) async { + developer.log('🔍 Checking login status...', name: 'MyPostCardBloc'); + emit(const MyPostCardCheckingLogin()); + + try { + final isLogin = await LocalPreference.getLogin(); + developer.log('📊 Login status: $isLogin', name: 'MyPostCardBloc'); + + if (isLogin) { + developer.log('✅ User is logged in - initializing state', name: 'MyPostCardBloc'); + // User is logged in, initialize with empty lists and loading states + emit(const MyPostCardLoaded( + draftPostCards: [], + orderPostCards: [], + isDraftLoading: true, + isOrderLoading: true, + )); + + // Fetch both drafts and orders + add(const FetchDraftPostCards()); + add(const FetchOrderPostCards()); + } else { + developer.log('❌ User is NOT logged in - emitting MyPostCardNotLoggedIn', name: 'MyPostCardBloc'); + // User is not logged in + emit(const MyPostCardNotLoggedIn()); + } + } catch (error) { + developer.log('âš ī¸ Error checking login: $error', name: 'MyPostCardBloc'); + // If there's an error checking login, treat as not logged in + emit(const MyPostCardNotLoggedIn()); + } + } + + /// Handle fetching draft postcards + Future _onFetchDraftPostCards( + FetchDraftPostCards event, + Emitter emit, + ) async { + developer.log('đŸ“Ĩ Fetching draft postcards...', name: 'MyPostCardBloc'); + // Get current state + final currentState = state; + + if (currentState is MyPostCardLoaded) { + // Set draft loading to true + emit(currentState.copyWith(isDraftLoading: true)); + } + + try { + final draftPostCards = await repository.fetchMyPostCards(type: 'draft'); + developer.log('✅ Draft postcards fetched: ${draftPostCards.length} items', name: 'MyPostCardBloc'); + + if (state is MyPostCardLoaded) { + // Update with fetched drafts + emit((state as MyPostCardLoaded).copyWith( + draftPostCards: draftPostCards, + isDraftLoading: false, + )); + } else { + developer.log('âš ī¸ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc'); + // Fallback: create new loaded state (shouldn't normally happen) + emit(MyPostCardLoaded( + draftPostCards: draftPostCards, + orderPostCards: const [], + isDraftLoading: false, + isOrderLoading: false, + )); + } + } catch (error) { + developer.log('❌ Error fetching drafts: $error', name: 'MyPostCardBloc'); + // Keep current lists but stop loading + if (state is MyPostCardLoaded) { + emit((state as MyPostCardLoaded).copyWith(isDraftLoading: false)); + } + + // Emit error state + emit(MyPostCardError( + errorMessage: error.toString(), + errorType: 'draft', + )); + } + } + + /// Handle fetching order postcards + Future _onFetchOrderPostCards( + FetchOrderPostCards event, + Emitter emit, + ) async { + developer.log('đŸ“Ĩ Fetching order postcards...', name: 'MyPostCardBloc'); + // Get current state + final currentState = state; + + if (currentState is MyPostCardLoaded) { + // Set order loading to true + emit(currentState.copyWith(isOrderLoading: true)); + } + + try { + final orderPostCards = await repository.fetchMyPostCards(type: 'orders'); + developer.log('✅ Order postcards fetched: ${orderPostCards.length} items', name: 'MyPostCardBloc'); + + if (state is MyPostCardLoaded) { + // Update with fetched orders + emit((state as MyPostCardLoaded).copyWith( + orderPostCards: orderPostCards, + isOrderLoading: false, + )); + } else { + developer.log('âš ī¸ State is not MyPostCardLoaded, creating new state', name: 'MyPostCardBloc'); + // Fallback: create new loaded state (shouldn't normally happen) + emit(MyPostCardLoaded( + draftPostCards: const [], + orderPostCards: orderPostCards, + isDraftLoading: false, + isOrderLoading: false, + )); + } + } catch (error) { + developer.log('❌ Error fetching orders: $error', name: 'MyPostCardBloc'); + // Keep current lists but stop loading + if (state is MyPostCardLoaded) { + emit((state as MyPostCardLoaded).copyWith(isOrderLoading: false)); + } + + // Emit error state + emit(MyPostCardError( + errorMessage: error.toString(), + errorType: 'order', + )); + } + } + + /// Handle refreshing draft postcards + Future _onRefreshDraftPostCards( + RefreshDraftPostCards event, + Emitter emit, + ) async { + developer.log('🔄 Refreshing draft postcards...', name: 'MyPostCardBloc'); + try { + final draftPostCards = await repository.fetchMyPostCards(type: 'draft'); + developer.log('✅ Draft postcards refreshed: ${draftPostCards.length} items', name: 'MyPostCardBloc'); + + if (state is MyPostCardLoaded) { + emit((state as MyPostCardLoaded).copyWith( + draftPostCards: draftPostCards, + )); + } + } catch (error) { + developer.log('❌ Error refreshing drafts: $error', name: 'MyPostCardBloc'); + emit(MyPostCardError( + errorMessage: error.toString(), + errorType: 'draft', + )); + } + } + + /// Handle refreshing order postcards + Future _onRefreshOrderPostCards( + RefreshOrderPostCards event, + Emitter emit, + ) async { + developer.log('🔄 Refreshing order postcards...', name: 'MyPostCardBloc'); + try { + final orderPostCards = await repository.fetchMyPostCards(type: 'orders'); + developer.log('✅ Order postcards refreshed: ${orderPostCards.length} items', name: 'MyPostCardBloc'); + + if (state is MyPostCardLoaded) { + emit((state as MyPostCardLoaded).copyWith( + orderPostCards: orderPostCards, + )); + } + } catch (error) { + developer.log('❌ Error refreshing orders: $error', name: 'MyPostCardBloc'); + emit(MyPostCardError( + errorMessage: error.toString(), + errorType: 'order', + )); + } + } +} \ No newline at end of file diff --git a/lib/postcard/blocs/myPostCards/my_postcard_event.dart b/lib/postcard/blocs/myPostCards/my_postcard_event.dart new file mode 100644 index 0000000..7c25209 --- /dev/null +++ b/lib/postcard/blocs/myPostCards/my_postcard_event.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; + +abstract class MyPostCardEvent extends Equatable { + const MyPostCardEvent(); + + @override + List get props => []; +} + +/// Event to check login status +class CheckLoginStatus extends MyPostCardEvent { + const CheckLoginStatus(); +} + +/// Event to fetch draft postcards +class FetchDraftPostCards extends MyPostCardEvent { + const FetchDraftPostCards(); +} + +/// Event to fetch order postcards +class FetchOrderPostCards extends MyPostCardEvent { + const FetchOrderPostCards(); +} + +/// Event to refresh draft postcards +class RefreshDraftPostCards extends MyPostCardEvent { + const RefreshDraftPostCards(); +} + +/// Event to refresh order postcards +class RefreshOrderPostCards extends MyPostCardEvent { + const RefreshOrderPostCards(); +} \ No newline at end of file diff --git a/lib/postcard/blocs/myPostCards/my_postcard_state.dart b/lib/postcard/blocs/myPostCards/my_postcard_state.dart new file mode 100644 index 0000000..cef9dd7 --- /dev/null +++ b/lib/postcard/blocs/myPostCards/my_postcard_state.dart @@ -0,0 +1,76 @@ +import 'package:equatable/equatable.dart'; +import '../../models/my_postcard_model.dart'; + +abstract class MyPostCardState extends Equatable { + const MyPostCardState(); + + @override + List get props => []; +} + +/// Initial state +class MyPostCardInitial extends MyPostCardState { + const MyPostCardInitial(); +} + +/// State to check login status +class MyPostCardCheckingLogin extends MyPostCardState { + const MyPostCardCheckingLogin(); +} + +/// State when user is not logged in +class MyPostCardNotLoggedIn extends MyPostCardState { + const MyPostCardNotLoggedIn(); +} + +/// Combined state that holds both drafts and orders +class MyPostCardLoaded extends MyPostCardState { + final List draftPostCards; + final List orderPostCards; + final bool isDraftLoading; + final bool isOrderLoading; + + const MyPostCardLoaded({ + required this.draftPostCards, + required this.orderPostCards, + this.isDraftLoading = false, + this.isOrderLoading = false, + }); + + @override + List get props => [ + draftPostCards, + orderPostCards, + isDraftLoading, + isOrderLoading, + ]; + + /// Helper method to create a copy with updated values + MyPostCardLoaded copyWith({ + List? draftPostCards, + List? orderPostCards, + bool? isDraftLoading, + bool? isOrderLoading, + }) { + return MyPostCardLoaded( + draftPostCards: draftPostCards ?? this.draftPostCards, + orderPostCards: orderPostCards ?? this.orderPostCards, + isDraftLoading: isDraftLoading ?? this.isDraftLoading, + isOrderLoading: isOrderLoading ?? this.isOrderLoading, + ); + } +} + +/// Error state +class MyPostCardError extends MyPostCardState { + final String errorMessage; + final String errorType; // 'draft' or 'order' + + const MyPostCardError({ + required this.errorMessage, + required this.errorType, + }); + + @override + List get props => [errorMessage, errorType]; +} \ No newline at end of file diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart new file mode 100644 index 0000000..016b511 --- /dev/null +++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_bloc.dart @@ -0,0 +1,243 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/postcard_checkout_repository.dart'; +import 'postcard_checkout_event.dart'; +import 'postcard_checkout_state.dart'; + +class PostcardCheckoutBloc + extends Bloc { + final CreatePostCardRepository repository; + + PostcardCheckoutBloc({required this.repository}) + : super(const PostcardCheckoutState()) { + on(_onUpdateAddress); + on(_onUpdateContent); + on(_onUpdateCheckoutData); + on(_onSaveAsDraft); + on(_onSubmitPostcard); + on(_onConfirmPayment); // 🆕 NEW + } + + void _onUpdateAddress( + UpdateAddressEvent event, Emitter emit) { + emit(state.copyWith( + countryName: event.countryName, + cityName: event.cityName, + stateName: event.stateName, + zipCode: event.zipCode, + address1: event.address1, + address2: event.address2, + )); + } + + void _onUpdateContent( + UpdatePostcardContentEvent event, Emitter emit) { + emit(state.copyWith( + pcTitle: event.pcTitle, + pcContent: event.pcContent, + pcImageFile: event.pcImageFile, + )); + } + + void _onUpdateCheckoutData( + UpdateCheckoutDataEvent event, Emitter emit) { + emit(state.copyWith( + countryName: event.countryName, + cityName: event.cityName, + stateName: event.stateName, + zipCode: event.zipCode, + address1: event.address1, + address2: event.address2, + pcTitle: event.pcTitle, + pcContent: event.pcContent, + pcImageFile: event.pcImageFile, + pcNumber: event.pcNumber, + pcDatetime: event.pcDatetime, + fullname: event.fullname, + emailAddress: event.emailAddress, + mobileNumber: event.mobileNumber, + isdCode: event.isdCode, + isForSelf: event.isForSelf, + baseAmount: event.baseAmount, + totalTaxAmount: event.totalTaxAmount, + totalAmount: event.totalAmount, + )); + } + + Future _onSaveAsDraft( + SaveAsDraftEvent event, Emitter emit) async { + emit(state.copyWith(isLoading: true, error: null, isSuccess: false)); + + try { + // Validate that image file exists before submitting + if (state.pcImageFile == null) { + emit(state.copyWith( + isLoading: false, + error: 'Please select a postcard image', + isSuccess: false, + )); + 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, + ); + + // Extract order ID from response if available + final orderId = response['orderId']?.toString() ?? + response['order_id']?.toString() ?? + response['id']?.toString(); + + emit(state.copyWith( + isLoading: false, + isSuccess: true, + isDraft: true, + orderId: orderId, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + error: e.toString(), + isSuccess: false, + )); + } + } + + Future _onSubmitPostcard( + SubmitPostcardEvent event, Emitter emit) async { + emit(state.copyWith(isLoading: true, error: null, isSuccess: false)); + + try { + // Validate that image file exists before submitting + if (state.pcImageFile == null) { + emit(state.copyWith( + isLoading: false, + error: 'Please select a postcard image', + isSuccess: false, + )); + 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, + ); + + // 🆕 Parse response from backend + // Expected: {"postcardId": 16, "clientSecret": "pi_3Sx0yjRtCkWyT4Em1MKw1FeU_secret_S8M74wnEhTRC9lUz9RqJnuuqg"} + + final postcardId = response['postcardId'] as int?; + final clientSecret = response['clientSecret'] 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', + isSuccess: false, + )); + return; + } + + // 🆕 Emit success with clientSecret for payment processing + emit(state.copyWith( + isLoading: false, + isSuccess: true, + isDraft: false, + postcardId: postcardId, + clientSecret: clientSecret, // This will trigger payment flow + orderId: orderId, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + error: e.toString(), + isSuccess: false, + )); + } + } + + /// 🆕 Confirm payment after Stripe payment completes + /// This should be called after Stripe payment succeeds or fails + Future _onConfirmPayment( + ConfirmPaymentEvent event, Emitter emit) async { + + // Validate postcardId exists + if (state.postcardId == null) { + emit(state.copyWith( + confirmationError: 'Cannot confirm payment - postcard ID is missing', + isConfirmingPayment: false, + isPaymentConfirmed: false, + )); + return; + } + + emit(state.copyWith( + isConfirmingPayment: true, + confirmationError: null, + isPaymentConfirmed: false, + )); + + try { + final response = await repository.confirmPayment( + postcardId: state.postcardId!, + stripeStatus: event.stripeStatus, + paymentStatus: event.paymentStatus, + ); + + // Payment confirmation successful + emit(state.copyWith( + isConfirmingPayment: false, + isPaymentConfirmed: true, + confirmationError: null, + )); + } catch (e) { + emit(state.copyWith( + isConfirmingPayment: false, + isPaymentConfirmed: false, + confirmationError: e.toString(), + )); + } + } +} \ No newline at end of file diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart new file mode 100644 index 0000000..765e6a7 --- /dev/null +++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_event.dart @@ -0,0 +1,97 @@ +import 'dart:io'; + +abstract class PostcardCheckoutEvent {} + +/// Page 1 – Address +class UpdateAddressEvent extends PostcardCheckoutEvent { + final String countryName; + final String cityName; + final String stateName; + final String zipCode; + final String address1; + final String address2; + + UpdateAddressEvent({ + required this.countryName, + required this.cityName, + required this.stateName, + required this.zipCode, + required this.address1, + required this.address2, + }); +} + +/// Page 2 – Postcard content +class UpdatePostcardContentEvent extends PostcardCheckoutEvent { + final String pcTitle; + final String pcContent; + final File? pcImageFile; // ⭐ CHANGED: File instead of String + + UpdatePostcardContentEvent({ + required this.pcTitle, + required this.pcContent, + this.pcImageFile, // ⭐ CHANGED: nullable File + }); +} + +/// Update all checkout data at once +class UpdateCheckoutDataEvent extends PostcardCheckoutEvent { + 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; // ⭐ CHANGED: File instead of String + final String? pcNumber; + final String? pcDatetime; + final String? fullname; + final String? emailAddress; + final String? mobileNumber; + final String? isdCode; + final bool? isForSelf; + final double? baseAmount; + final double? totalTaxAmount; + final double? totalAmount; + + UpdateCheckoutDataEvent({ + this.countryName, + this.cityName, + this.stateName, + this.zipCode, + this.address1, + this.address2, + this.pcTitle, + this.pcContent, + this.pcImageFile, // ⭐ CHANGED + this.pcNumber, + this.pcDatetime, + this.fullname, + this.emailAddress, + this.mobileNumber, + this.isdCode, + this.isForSelf, + this.baseAmount, + this.totalTaxAmount, + this.totalAmount, + }); +} + +/// Save as draft +class SaveAsDraftEvent extends PostcardCheckoutEvent {} + +/// Page 3 – Checkout & submit (Pay button) +class SubmitPostcardEvent extends PostcardCheckoutEvent {} + +/// 🆕 Confirm payment after successful Stripe payment +class ConfirmPaymentEvent extends PostcardCheckoutEvent { + final String stripeStatus; // e.g., "succeeded", "requires_payment_method" + final String paymentStatus; // e.g., "success", "failed" + + ConfirmPaymentEvent({ + required this.stripeStatus, + required this.paymentStatus, + }); +} \ No newline at end of file diff --git a/lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart b/lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart new file mode 100644 index 0000000..ff165bb --- /dev/null +++ b/lib/postcard/blocs/postcardCheckout/postcard_checkout_state.dart @@ -0,0 +1,136 @@ +import 'dart:io'; + +class PostcardCheckoutState { + 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; + + final bool isForSelf; + final bool isDraft; + + final double baseAmount; + final double totalTaxAmount; + final double totalAmount; + + final bool isLoading; + final String? error; + final bool isSuccess; + final String? orderId; + final String? clientSecret; // 🆕 NEW: For Stripe payment + final int? postcardId; // 🆕 NEW: Postcard ID from API + + // 🆕 Payment confirmation tracking + final bool isConfirmingPayment; // Loading state for payment confirmation + final bool isPaymentConfirmed; // Whether payment was confirmed successfully + final String? confirmationError; // Error during payment confirmation + + const PostcardCheckoutState({ + this.countryName = '', + this.cityName = '', + this.stateName = '', + this.zipCode = '', + this.address1 = '', + this.address2 = '', + this.pcTitle = '', + this.pcContent = '', + this.pcImageFile, + this.pcNumber = '', + this.pcDatetime = '', + this.fullname = '', + this.emailAddress = '', + this.mobileNumber = '', + this.isdCode = '', + this.isForSelf = true, + this.isDraft = true, + this.baseAmount = 0, + this.totalTaxAmount = 0, + this.totalAmount = 0, + this.isLoading = false, + this.error, + this.isSuccess = false, + this.orderId, + this.clientSecret, // 🆕 NEW + this.postcardId, // 🆕 NEW + this.isConfirmingPayment = false, // 🆕 NEW + this.isPaymentConfirmed = false, // 🆕 NEW + this.confirmationError, // 🆕 NEW + }); + + PostcardCheckoutState copyWith({ + String? countryName, + String? cityName, + String? stateName, + String? zipCode, + String? address1, + String? address2, + String? pcTitle, + String? pcContent, + File? pcImageFile, + String? pcNumber, + String? pcDatetime, + String? fullname, + String? emailAddress, + String? mobileNumber, + String? isdCode, + bool? isForSelf, + bool? isDraft, + double? baseAmount, + double? totalTaxAmount, + double? totalAmount, + bool? isLoading, + String? error, + bool? isSuccess, + String? orderId, + String? clientSecret, // 🆕 NEW + int? postcardId, // 🆕 NEW + bool? isConfirmingPayment, // 🆕 NEW + bool? isPaymentConfirmed, // 🆕 NEW + String? confirmationError, // 🆕 NEW + }) { + return PostcardCheckoutState( + countryName: countryName ?? this.countryName, + cityName: cityName ?? this.cityName, + stateName: stateName ?? this.stateName, + zipCode: zipCode ?? this.zipCode, + address1: address1 ?? this.address1, + address2: address2 ?? this.address2, + pcTitle: pcTitle ?? this.pcTitle, + pcContent: pcContent ?? this.pcContent, + pcImageFile: pcImageFile ?? this.pcImageFile, + pcNumber: pcNumber ?? this.pcNumber, + pcDatetime: pcDatetime ?? this.pcDatetime, + fullname: fullname ?? this.fullname, + emailAddress: emailAddress ?? this.emailAddress, + mobileNumber: mobileNumber ?? this.mobileNumber, + isdCode: isdCode ?? this.isdCode, + isForSelf: isForSelf ?? this.isForSelf, + isDraft: isDraft ?? this.isDraft, + baseAmount: baseAmount ?? this.baseAmount, + totalTaxAmount: totalTaxAmount ?? this.totalTaxAmount, + totalAmount: totalAmount ?? this.totalAmount, + isLoading: isLoading ?? this.isLoading, + error: error, + isSuccess: isSuccess ?? this.isSuccess, + orderId: orderId ?? this.orderId, + clientSecret: clientSecret ?? this.clientSecret, // 🆕 NEW + postcardId: postcardId ?? this.postcardId, // 🆕 NEW + isConfirmingPayment: isConfirmingPayment ?? this.isConfirmingPayment, // 🆕 NEW + isPaymentConfirmed: isPaymentConfirmed ?? this.isPaymentConfirmed, // 🆕 NEW + confirmationError: confirmationError, // 🆕 NEW + ); + } +} \ No newline at end of file diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index 749dcaf..ac1d4d8 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -72,6 +72,19 @@ class PostcardCreationBloc } }); + on((event, emit) { + emit(state.copyWith( + pcTitle: event.pcTitle, + fullName: event.fullName, + emailId: event.emailId, + phoneNumber: event.phoneNumber, + city: event.city, + country: event.country, + state: event.state, + zipCode: event.zipCode, + )); + }); + /* Select filter */ on((event, emit) async { // 1ī¸âƒŖ No image? Exit early. diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart index 737885f..0439cab 100644 --- a/lib/postcard/blocs/postcard_creation_events.dart +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -36,4 +36,25 @@ class TogglePurchaseOption extends PostcardCreationEvent { final bool isGift; TogglePurchaseOption(this.isGift); +} +class UpdatePurchaseFormData extends PostcardCreationEvent { + final String? pcTitle; + final String? fullName; + final String? emailId; + final String? phoneNumber; + final String? city; + final String? country; + final String? state; + final String? zipCode; + + UpdatePurchaseFormData({ + this.pcTitle, + this.fullName, + this.emailId, + this.phoneNumber, + this.city, + this.country, + this.state, + this.zipCode, + }); } \ 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 d8fec4a..44191e1 100644 --- a/lib/postcard/blocs/postcard_creation_state.dart +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -10,6 +10,16 @@ class PostcardCreationState { final bool isProcessing; final String? selectedFont; + // Add these new fields + final String? pcTitle; + final String? fullName; + final String? emailId; + final String? phoneNumber; + final String? city; + final String? country; + final String? state; + final String? zipCode; + const PostcardCreationState({ required this.currentStep, this.imagePath, @@ -18,7 +28,15 @@ class PostcardCreationState { this.message, this.isGift = false, this.isProcessing = false, - this.selectedFont + this.selectedFont, + this.pcTitle, + this.fullName, + this.emailId, + this.phoneNumber, + this.city, + this.country, + this.state, + this.zipCode, }); PostcardCreationState copyWith({ @@ -30,6 +48,14 @@ class PostcardCreationState { bool? isGift, bool? isProcessing, String? selectedFont, + String? pcTitle, + String? fullName, + String? emailId, + String? phoneNumber, + String? city, + String? country, + String? state, + String? zipCode, }) { return PostcardCreationState( currentStep: currentStep ?? this.currentStep, @@ -39,7 +65,15 @@ class PostcardCreationState { message: message ?? this.message, isGift: isGift ?? this.isGift, isProcessing: isProcessing ?? this.isProcessing, - selectedFont: selectedFont ?? this.selectedFont + selectedFont: selectedFont ?? this.selectedFont, + pcTitle: pcTitle ?? this.pcTitle, + fullName: fullName ?? this.fullName, + emailId: emailId ?? this.emailId, + phoneNumber: phoneNumber ?? this.phoneNumber, + city: city ?? this.city, + country: country ?? this.country, + state: state ?? this.state, + zipCode: zipCode ?? this.zipCode, ); } } \ No newline at end of file diff --git a/lib/postcard/models/my_postcard_model.dart b/lib/postcard/models/my_postcard_model.dart new file mode 100644 index 0000000..1ff827e --- /dev/null +++ b/lib/postcard/models/my_postcard_model.dart @@ -0,0 +1,173 @@ +class MyPostCard { + final int id; + final int userXid; + final String pcTitle; + final String pcNumber; + final String cityName; + final DateTime pcDatetime; + final String pcContent; + final String pcImagePath; + final bool isForSelf; + final String fullname; + final String emailAddress; + final String isdCode; + final String mobileNumber; + final String address1; + final String address2; + final String zipCode; + final String stateName; + final String countryName; + final String orderStatus; + final double baseAmount; + final int? couponXid; + final double? couponDiscountPercent; + final double? couponDiscountAmount; + final double totalTaxAmount; + final double totalAmount; + final bool isPaid; + final String paymentMode; + final String? paymentId; + final String paymentStatus; + final String? paymentIntentId; + final bool isDraft; + final DateTime? deliveredOn; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + + MyPostCard({ + required this.id, + required this.userXid, + required this.pcTitle, + required this.pcNumber, + required this.cityName, + required this.pcDatetime, + required this.pcContent, + required this.pcImagePath, + required this.isForSelf, + required this.fullname, + required this.emailAddress, + required this.isdCode, + required this.mobileNumber, + required this.address1, + required this.address2, + required this.zipCode, + required this.stateName, + required this.countryName, + required this.orderStatus, + required this.baseAmount, + this.couponXid, + this.couponDiscountPercent, + this.couponDiscountAmount, + required this.totalTaxAmount, + required this.totalAmount, + required this.isPaid, + required this.paymentMode, + this.paymentId, + required this.paymentStatus, + this.paymentIntentId, + required this.isDraft, + this.deliveredOn, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory MyPostCard.fromJson(Map json) { + return MyPostCard( + id: json['id'] ?? 0, + userXid: json['userXid'] ?? 0, + pcTitle: json['pcTitle'] ?? 'N/A', + pcNumber: json['pcNumber'] ?? 'N/A', + cityName: json['cityName'] ?? 'N/A', + pcDatetime: json['pcDatetime'] != null + ? DateTime.parse(json['pcDatetime']) + : DateTime.now(), + pcContent: json['pcContent'] ?? 'N/A', + pcImagePath: json['pcImagePath'] ?? '', + isForSelf: json['isForSelf'] ?? false, + fullname: json['fullname'] ?? 'N/A', + emailAddress: json['emailAddress'] ?? 'N/A', + isdCode: json['isdCode'] ?? '', + mobileNumber: json['mobileNumber'] ?? '', + address1: json['address1'] ?? 'N/A', + address2: json['address2'] ?? '', + zipCode: json['zipCode'] ?? '', + stateName: json['stateName'] ?? 'N/A', + countryName: json['countryName'] ?? 'N/A', + orderStatus: json['orderStatus'] ?? 'N/A', + baseAmount: json['baseAmount'] != null + ? (json['baseAmount'] as num).toDouble() + : 0.0, + couponXid: json['couponXid'], + couponDiscountPercent: json['couponDiscountPercent'] != null + ? (json['couponDiscountPercent'] as num).toDouble() + : null, + couponDiscountAmount: json['couponDiscountAmount'] != null + ? (json['couponDiscountAmount'] as num).toDouble() + : null, + totalTaxAmount: json['totalTaxAmount'] != null + ? (json['totalTaxAmount'] as num).toDouble() + : 0.0, + totalAmount: json['totalAmount'] != null + ? (json['totalAmount'] as num).toDouble() + : 0.0, + isPaid: json['isPaid'] ?? false, + paymentMode: json['paymentMode'] ?? 'N/A', + paymentId: json['paymentId'], + paymentStatus: json['paymentStatus'] ?? 'N/A', + paymentIntentId: json['paymentIntentId'], + isDraft: json['isDraft'] ?? false, + deliveredOn: json['deliveredOn'] != null + ? DateTime.parse(json['deliveredOn']) + : null, + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt']) + : DateTime.now(), + ); + } + + Map toJson() { + return { + 'id': id, + 'userXid': userXid, + 'pcTitle': pcTitle, + 'pcNumber': pcNumber, + 'cityName': cityName, + 'pcDatetime': pcDatetime.toIso8601String(), + 'pcContent': pcContent, + 'pcImagePath': pcImagePath, + 'isForSelf': isForSelf, + 'fullname': fullname, + 'emailAddress': emailAddress, + 'isdCode': isdCode, + 'mobileNumber': mobileNumber, + 'address1': address1, + 'address2': address2, + 'zipCode': zipCode, + 'stateName': stateName, + 'countryName': countryName, + 'orderStatus': orderStatus, + 'baseAmount': baseAmount, + 'couponXid': couponXid, + 'couponDiscountPercent': couponDiscountPercent, + 'couponDiscountAmount': couponDiscountAmount, + 'totalTaxAmount': totalTaxAmount, + 'totalAmount': totalAmount, + 'isPaid': isPaid, + 'paymentMode': paymentMode, + 'paymentId': paymentId, + 'paymentStatus': paymentStatus, + 'paymentIntentId': paymentIntentId, + 'isDraft': isDraft, + 'deliveredOn': deliveredOn?.toIso8601String(), + 'isActive': isActive, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + }; + } +} diff --git a/lib/postcard/repository/my_postcard_repository.dart b/lib/postcard/repository/my_postcard_repository.dart new file mode 100644 index 0000000..2a5932b --- /dev/null +++ b/lib/postcard/repository/my_postcard_repository.dart @@ -0,0 +1,20 @@ +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../models/my_postcard_model.dart'; + +class MyPostCardsRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Fetch My Postcards (draft / orders) + Future> fetchMyPostCards({ + required String type, // "draft" or "orders" + }) async { + final response = await _apiService.getApi( + url: '${ApiUrls.myPostCards}?type=$type', + ); + + return (response.data as List) + .map((e) => MyPostCard.fromJson(e)) + .toList(); + } +} diff --git a/lib/postcard/repository/postcard_checkout_repository.dart b/lib/postcard/repository/postcard_checkout_repository.dart new file mode 100644 index 0000000..3d82050 --- /dev/null +++ b/lib/postcard/repository/postcard_checkout_repository.dart @@ -0,0 +1,205 @@ +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 CreatePostCardRepository { + final NetworkApiService _apiServices = NetworkApiService(); + + /// Create / Save Postcard (Draft or Final) + /// ⭐ UPDATED: Now uses multipart/form-data for file upload + 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 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', isDraft.toString()), + MapEntry('baseAmount', baseAmount.toString()), + MapEntry('totalTaxAmount', totalTaxAmount.toString()), + MapEntry('totalAmount', totalAmount.toString()), + ]); + + // 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/views/my_orders_page_view.dart b/lib/postcard/views/my_orders_page_view.dart deleted file mode 100644 index f21470f..0000000 --- a/lib/postcard/views/my_orders_page_view.dart +++ /dev/null @@ -1,817 +0,0 @@ -import 'dart:io'; - -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:citycards_customer/postcard/blocs/postcard_creation_events.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:google_fonts/google_fonts.dart'; - -import '../blocs/postcard_creation_bloc.dart'; -import '../blocs/postcard_creation_state.dart'; - -class MyOrdersPageView extends StatefulWidget { - const MyOrdersPageView({super.key}); - - @override - State createState() => _MyOrdersPageViewState(); -} - -class _MyOrdersPageViewState extends State { - bool showDrafts = true; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final bloc = context.read(); - return SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // đŸ™ī¸ Header - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - - Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => setState(() => showDrafts = true), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: showDrafts - ? const Color(0xffF95F62).withOpacity(0.24) - : Colors.transparent, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: showDrafts - ? const Color(0xffF95F62).withOpacity(0.4) - : const Color(0xffE0E0E0), - ), - ), - child: Center( - child: Text( - "My drafts", - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14.sp, - color: showDrafts - ? Colors.black - : Colors.black.withOpacity(0.56), - ), - ), - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: GestureDetector( - onTap: () => setState(() => showDrafts = false), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: !showDrafts - ? const Color(0xffF95F62).withOpacity(0.24) - : Colors.transparent, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: !showDrafts - ? const Color(0xffF95F62).withOpacity(0.4) - : const Color(0xffE0E0E0), - ), - ), - child: Center( - child: Text( - "My orders", - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14.sp, - color: !showDrafts - ? Colors.black - : Colors.black.withOpacity(0.56), - ), - ), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 24), - - // đŸ“Ŧ Postcard List - showDrafts - ? Expanded( - child: ListView.builder( - itemCount: 5, - itemBuilder: (context, index) { - return Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.fromLTRB( - 10, - 10, - 10, - 10, - ), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xffF1F5F7), - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file( - File(state.imagePath ?? ""), - height: 90.h, - width: 90.w, - fit: BoxFit.cover, - ), - ), - const SizedBox(width: 20), - - Expanded( - child: SizedBox( - height: 90.h, - child: Stack( - children: [ - /// Centered texts - Align( - alignment: Alignment.centerLeft, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "#688574", - style: GoogleFonts.poppins( - fontSize: 14.sp, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - ), - const SizedBox(height: 3), - Text( - "My postcard", - style: GoogleFonts.poppins( - fontSize: 16.sp, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - ), - ], - ), - ), - - /// 🧭 Bottom-right icons - Align( - alignment: Alignment.bottomRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - InkWell( - onTap: () {}, - child: Image.asset( - "assets/icons/delete_icon.png", - scale: 4, - ), - ), - const SizedBox(width: 20), - InkWell( - onTap: () {}, - child: Image.asset( - "assets/icons/edit_icon.png", - scale: 4, - ), - ), - const SizedBox(width: 20), - InkWell( - onTap: () {}, - child: Image.asset( - "assets/icons/send_icon.png", - scale: 4, - ), - ), - const SizedBox(width: 10), - ], - ), - ), - ], - ), - ), - ), - ], - ), - ); - }, - ), - ) - : Expanded( - child: ListView.builder( - itemCount: 2, - itemBuilder: (context, index) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "#688574", - style: GoogleFonts.poppins( - fontSize: 14.sp, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - ), - const SizedBox(height: 3), - Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.fromLTRB( - 10, - 10, - 10, - 10, - ), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xffF1F5F7), - ), - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.file( - File(state.imagePath ?? ""), - height: 70.h, - width: 70.w, - fit: BoxFit.cover, - ), - ), - const SizedBox(width: 20), - - Expanded( - child: SizedBox( - height: 60.h, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.start, - children: [ - Text( - "My PostCard", - style: GoogleFonts.poppins( - color: Colors.black, - fontWeight: - FontWeight.w400, - fontSize: 16.sp, - ), - ), - const SizedBox(height: 6), - Text( - "5 Post cards", - style: GoogleFonts.poppins( - color: Colors.black, - fontWeight: - FontWeight.w400, - fontSize: 14.sp, - ), - ), - ], - ), - Column( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - Container( - padding: EdgeInsets.fromLTRB(13, 7, 13, 7), - decoration: BoxDecoration( - color: Color( - 0xff00FFA6, - ).withOpacity(0.16), - border: Border.all( - color: Color( - 0xff439F6E, - ), - ), - borderRadius: - BorderRadius.circular( - 16, - ), - ), - child: Text( - "In Progress", - style: TextStyle( - color: Colors.black, - fontWeight: - FontWeight.w400, - fontSize: 8.54.sp, - ), - ), - ), - InkWell( - onTap: () { - bloc.add(GoToNextStep()); - }, - child: Row( - children: [ - Icon( - Icons - .remove_red_eye_outlined, - size: 15, - color: Color( - 0xffF95F62, - ), - ), - SizedBox(width: 5.w), - Text( - "Preview", - style: TextStyle( - fontWeight: - FontWeight.w400, - color: Color( - 0xffF95F62, - ), - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ], - ); - }, - ), - ), - - // ➕ Create postcard button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), - ), - ), - child: Text( - "Create post card", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -// import 'package:citycards_customer/common_packages/app_bar.dart'; -// import 'package:citycards_customer/core/route_constants.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter_screenutil/flutter_screenutil.dart'; -// import 'package:google_fonts/google_fonts.dart'; -// -// class MyOrdersPageView extends StatefulWidget { -// const MyOrdersPageView({super.key}); -// -// @override -// State createState() => _MyOrdersPageViewState(); -// } -// -// class _MyOrdersPageViewState extends State { -// bool showDrafts = true; -// -// @override -// Widget build(BuildContext context) { -// return SafeArea( -// child: Padding( -// padding: const EdgeInsets.all(16), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// // đŸ™ī¸ Header -// CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), -// -// Row( -// children: [ -// Expanded( -// child: GestureDetector( -// onTap: () => setState(() => showDrafts = true), -// child: Container( -// padding: const EdgeInsets.symmetric(vertical: 12), -// decoration: BoxDecoration( -// color: showDrafts -// ? const Color(0xffF95F62).withOpacity(0.24) -// : Colors.transparent, -// borderRadius: BorderRadius.circular(12), -// border: Border.all( -// color: showDrafts -// ? const Color(0xffF95F62).withOpacity(0.4) -// : const Color(0xffE0E0E0), -// ), -// ), -// child: Center( -// child: Text( -// "My drafts", -// style: TextStyle( -// fontWeight: FontWeight.w400, -// fontSize: 14.sp, -// color: showDrafts -// ? Colors.black -// : Colors.black.withOpacity(0.56), -// ), -// ), -// ), -// ), -// ), -// ), -// const SizedBox(width: 12), -// Expanded( -// child: GestureDetector( -// onTap: () => setState(() => showDrafts = false), -// child: Container( -// padding: const EdgeInsets.symmetric(vertical: 12), -// decoration: BoxDecoration( -// color: !showDrafts -// ? const Color(0xffF95F62).withOpacity(0.24) -// : Colors.transparent, -// borderRadius: BorderRadius.circular(12), -// border: Border.all( -// color: !showDrafts -// ? const Color(0xffF95F62).withOpacity(0.4) -// : const Color(0xffE0E0E0), -// ), -// ), -// child: Center( -// child: Text( -// "My orders", -// style: TextStyle( -// fontWeight: FontWeight.w400, -// fontSize: 14.sp, -// color: !showDrafts -// ? Colors.black -// : Colors.black.withOpacity(0.56), -// ), -// ), -// ), -// ), -// ), -// ), -// ], -// ), -// const SizedBox(height: 24), -// -// // đŸ“Ŧ Postcard List -// showDrafts -// ? Expanded( -// child: ListView.builder( -// itemCount: 5, -// itemBuilder: (context, index) { -// return Container( -// margin: const EdgeInsets.only(bottom: 16), -// padding: const EdgeInsets.fromLTRB( -// 10, -// 10, -// 10, -// 10, -// ), -// decoration: BoxDecoration( -// color: const Color(0xFFFFF5F5), -// borderRadius: BorderRadius.circular(12), -// border: Border.all( -// color: const Color(0xffF1F5F7), -// ), -// ), -// child: Row( -// crossAxisAlignment: CrossAxisAlignment.center, -// children: [ -// ClipRRect( -// borderRadius: BorderRadius.circular(8), -// child: Container( -// height: 90.h, -// width: 90.w, -// color: const Color(0xffFEE7E7), -// child: const Icon( -// Icons.image_outlined, -// color: Color(0xffFDCDCE), -// size: 40, -// ), -// ), -// ), -// const SizedBox(width: 20), -// -// Expanded( -// child: SizedBox( -// height: 90.h, -// child: Stack( -// children: [ -// /// Centered texts -// Align( -// alignment: Alignment.centerLeft, -// child: Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: -// CrossAxisAlignment.start, -// children: [ -// Text( -// "#688574", -// style: GoogleFonts.poppins( -// fontSize: 16.sp, -// fontWeight: FontWeight.w400, -// color: Colors.black, -// ), -// ), -// const SizedBox(height: 4), -// Text( -// "Created 24 Jan 2025", -// style: GoogleFonts.poppins( -// fontSize: 12.sp, -// fontWeight: FontWeight.w400, -// color: -// const Color(0xff999999), -// ), -// ), -// ], -// ), -// ), -// -// /// Top-right buttons -// Positioned( -// top: 0, -// right: 0, -// child: Row( -// mainAxisSize: MainAxisSize.min, -// children: [ -// InkWell( -// onTap: () {}, -// child: Image.asset( -// "assets/icons/delete_icon.png", -// scale: 3.5, -// ), -// ), -// const SizedBox(width: 20), -// InkWell( -// onTap: () {}, -// child: Image.asset( -// "assets/icons/edit_icon.png", -// scale: 3.5, -// ), -// ), -// ], -// ), -// ), -// -// /// Bottom-right "Preview" link -// Positioned( -// bottom: 0, -// right: 0, -// child: InkWell( -// onTap: () { -// // Navigate to preview -// // You can use Navigator or your routing solution -// }, -// child: Row( -// children: [ -// const Icon( -// Icons.remove_red_eye_outlined, -// size: 15, -// color: Color(0xffF95F62), -// ), -// SizedBox(width: 5.w), -// Text( -// "Preview", -// style: TextStyle( -// fontWeight: FontWeight.w400, -// color: -// const Color(0xffF95F62), -// ), -// ), -// ], -// ), -// ), -// ), -// ], -// ), -// ), -// ), -// ], -// ), -// ); -// }, -// ), -// ) -// : Expanded( -// child: ListView.builder( -// itemCount: 3, -// itemBuilder: (context, index) { -// return Container( -// margin: const EdgeInsets.only(bottom: 16), -// padding: const EdgeInsets.fromLTRB( -// 16, -// 16, -// 16, -// 16, -// ), -// decoration: BoxDecoration( -// color: const Color(0xFFFFF5F5), -// borderRadius: BorderRadius.circular(12), -// border: Border.all( -// color: const Color(0xffF1F5F7), -// ), -// ), -// child: Row( -// crossAxisAlignment: -// CrossAxisAlignment.center, -// children: [ -// ClipRRect( -// borderRadius: BorderRadius.circular(8), -// child: Container( -// height: 70.h, -// width: 70.w, -// color: const Color(0xffFEE7E7), -// child: const Icon( -// Icons.image_outlined, -// color: Color(0xffFDCDCE), -// size: 30, -// ), -// ), -// ), -// const SizedBox(width: 20), -// -// Expanded( -// child: SizedBox( -// height: 60.h, -// child: Row( -// mainAxisAlignment: -// MainAxisAlignment.spaceBetween, -// children: [ -// Column( -// crossAxisAlignment: -// CrossAxisAlignment.start, -// mainAxisAlignment: -// MainAxisAlignment.start, -// children: [ -// Text( -// "My PostCard", -// style: GoogleFonts.poppins( -// color: Colors.black, -// fontWeight: -// FontWeight.w400, -// fontSize: 16.sp, -// ), -// ), -// const SizedBox(height: 6), -// Text( -// "5 Post cards", -// style: GoogleFonts.poppins( -// color: Colors.black, -// fontWeight: -// FontWeight.w400, -// fontSize: 14.sp, -// ), -// ), -// ], -// ), -// Column( -// mainAxisAlignment: -// MainAxisAlignment -// .spaceBetween, -// crossAxisAlignment: -// CrossAxisAlignment.end, -// children: [ -// Container( -// padding: EdgeInsets.fromLTRB(13, 7, 13, 7), -// decoration: BoxDecoration( -// color: Color( -// 0xff00FFA6, -// ).withOpacity(0.16), -// border: Border.all( -// color: Color( -// 0xff439F6E, -// ), -// ), -// borderRadius: -// BorderRadius.circular( -// 16, -// ), -// ), -// child: Text( -// "In Progress", -// style: TextStyle( -// color: Colors.black, -// fontWeight: -// FontWeight.w400, -// fontSize: 8.54.sp, -// ), -// ), -// ), -// InkWell( -// onTap: () { -// // Navigate to preview -// // You can use Navigator or your routing solution -// }, -// child: Row( -// children: [ -// Icon( -// Icons -// .remove_red_eye_outlined, -// size: 15, -// color: Color( -// 0xffF95F62, -// ), -// ), -// SizedBox(width: 5.w), -// Text( -// "Preview", -// style: TextStyle( -// fontWeight: -// FontWeight.w400, -// color: Color( -// 0xffF95F62, -// ), -// ), -// ), -// ], -// ), -// ), -// ], -// ), -// ], -// ), -// ), -// ), -// ], -// ), -// ); -// }, -// ), -// ), -// -// // ➕ Create postcard button -// SizedBox( -// width: double.infinity, -// child: ElevatedButton( -// onPressed: () { -// // Navigate to postcard creation flow (starts at upload photo step) -// Navigator.of(context).pushNamed(RouteConstants.uploadPhotoPage); -// }, -// style: ElevatedButton.styleFrom( -// backgroundColor: const Color(0xffF95F62), -// padding: EdgeInsets.symmetric(vertical: 16.h), -// shape: RoundedRectangleBorder( -// borderRadius: BorderRadius.circular(40), -// ), -// ), -// child: Text( -// "Create post card", -// style: GoogleFonts.poppins( -// color: Colors.white, -// fontSize: 14.sp, -// fontWeight: FontWeight.w600, -// ), -// ), -// ), -// ), -// ], -// ), -// ), -// ); -// } -// } diff --git a/lib/postcard/views/my_postcard_drafts_view.dart b/lib/postcard/views/my_postcard_drafts_view.dart new file mode 100644 index 0000000..a9a5502 --- /dev/null +++ b/lib/postcard/views/my_postcard_drafts_view.dart @@ -0,0 +1,290 @@ +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 '../../core/route_constants.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../blocs/myPostCards/my_postcard_bloc.dart'; +import '../blocs/myPostCards/my_postcard_event.dart'; +import '../blocs/myPostCards/my_postcard_state.dart'; +import '../models/my_postcard_model.dart'; + +class MyPostCardDraftView extends StatelessWidget { + const MyPostCardDraftView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + // Handle the new combined MyPostCardLoaded state + if (state is MyPostCardLoaded) { + // Show loading indicator if drafts are loading + if (state.isDraftLoading && state.draftPostCards.isEmpty) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xffF95F62), + ), + ); + } + + // Show empty state if no drafts + if (state.draftPostCards.isEmpty) { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Empty state image + Image.asset( + "assets/images/empty_postcard_drafts.png", + width: 260.w, + fit: BoxFit.contain, + ), + + SizedBox(height: 32.h), + + // Title + Text( + "Looks like you haven't created\nany postcards yet!", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: const Color(0xffF95F62), + height: 1.4, + ), + ), + + SizedBox(height: 12.h), + + // Subtitle + Text( + "Why not whip up a postcard and send it to someone special who's far away?", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: Colors.black54, + height: 1.5, + ), + ), + + SizedBox(height: 32.h), + ], + ), + ), + ); + } + + // Show the list of drafts + return RefreshIndicator( + onRefresh: () async { + context.read().add(const RefreshDraftPostCards()); + }, + color: const Color(0xffF95F62), + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: state.draftPostCards.length, + itemBuilder: (context, index) { + final postcard = state.draftPostCards[index]; + return _buildDraftCard(context, postcard); + }, + ), + ); + } + + // Handle error state + if (state is MyPostCardError && state.errorType == 'draft') { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 60, + color: Colors.red.withOpacity(0.6), + ), + const SizedBox(height: 16), + Text( + 'Error loading drafts', + style: GoogleFonts.poppins( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + state.errorMessage, + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 14.sp, + color: Colors.black54, + ), + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + context.read().add(const FetchDraftPostCards()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + 'Retry', + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + }, + ); + } + + Widget _buildDraftCard(BuildContext context, MyPostCard postcard) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xffF95F62).withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xffF1F5F7)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// LEFT IMAGE + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + '${ApiUrls.baseUrl}${postcard.pcImagePath}', + height: 72, + width: 72, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 72, + width: 72, + color: Colors.grey[300], + child: const Center( + child: CircularProgressIndicator( + color: Color(0xffF95F62), + strokeWidth: 2, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + height: 72, + width: 72, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, + ), + ), + + const SizedBox(width: 14), + + /// RIGHT CONTENT + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// NUMBER + Text( + "#${postcard.pcNumber}", + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + + const SizedBox(height: 4), + + /// TITLE + Text( + postcard.pcTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.poppins( + fontSize: 15, + fontWeight: FontWeight.w400, + color: Colors.black87, + ), + ), + + const SizedBox(height: 10), + + /// ICONS – BOTTOM RIGHT (UNDER TITLE) + Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + // delete + }, + child: Image.asset( + 'assets/icons/delete_icon.png', + width: 20, + height: 20, + ), + ), + const SizedBox(width: 16), + GestureDetector( + onTap: () { + // edit + }, + child: Image.asset( + 'assets/icons/edit_icon.png', + width: 20, + height: 20, + ), + ), + const SizedBox(width: 16), + GestureDetector( + onTap: () { + // send + }, + child: Image.asset( + 'assets/icons/send_icon.png', + width: 20, + height: 20, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/postcard/views/my_postcard_orders_view.dart b/lib/postcard/views/my_postcard_orders_view.dart new file mode 100644 index 0000000..74811ad --- /dev/null +++ b/lib/postcard/views/my_postcard_orders_view.dart @@ -0,0 +1,385 @@ +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 '../../core/route_constants.dart'; +import '../blocs/myPostCards/my_postcard_bloc.dart'; +import '../blocs/myPostCards/my_postcard_event.dart'; +import '../blocs/myPostCards/my_postcard_state.dart'; +import '../models/my_postcard_model.dart'; +import '../../networkApiServices/api_urls.dart'; +import 'my_postcard_preview_view.dart'; + +class MyPostCardOrdersView extends StatelessWidget { + const MyPostCardOrdersView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + // Handle the new combined MyPostCardLoaded state + if (state is MyPostCardLoaded) { + // Show loading indicator if orders are loading + if (state.isOrderLoading && state.orderPostCards.isEmpty) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xffF95F62), + ), + ); + } + + // Show empty state if no orders + if (state.orderPostCards.isEmpty) { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Empty state image + Image.asset( + "assets/images/empty_postcard_orders.png", + width: 260.w, + fit: BoxFit.contain, + ), + + SizedBox(height: 32.h), + + // Title + Text( + "It looks like you haven't ordered\na postcards yet!", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: const Color(0xffF95F62), + height: 1.4, + ), + ), + + SizedBox(height: 12.h), + + // Subtitle + Text( + "How about we whip up a fun postcard to send to your loved ones? Lets get started on that!", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: Colors.black54, + height: 1.5, + ), + ), + + SizedBox(height: 32.h), + ], + ), + ), + ); + } + + // Show the list of orders + return RefreshIndicator( + onRefresh: () async { + context.read().add(const RefreshOrderPostCards()); + }, + color: const Color(0xffF95F62), + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: state.orderPostCards.length, + itemBuilder: (context, index) { + final postcard = state.orderPostCards[index]; + return _buildOrderCard(context, postcard); + }, + ), + ); + } + + // Handle error state + if (state is MyPostCardError && state.errorType == 'order') { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 60, + color: Colors.red.withOpacity(0.6), + ), + const SizedBox(height: 16), + Text( + 'Error loading orders', + style: GoogleFonts.poppins( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + state.errorMessage, + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 14.sp, + color: Colors.black54, + ), + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + context.read().add(const FetchOrderPostCards()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + 'Retry', + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } + + return const SizedBox.shrink(); + }, + ); + } + + Widget _buildOrderCard(BuildContext context, MyPostCard postcard) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Postcard Number above the card + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text( + "#${postcard.pcNumber}", + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + ), + ), + + // Order Card + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xffF95F62).withValues(alpha:0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xffF1F5F7), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Postcard Image + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image( + image: NetworkImage('${ApiUrls.baseUrl}${postcard.pcImagePath}'), + height: 70.h, + width: 70.w, + fit: BoxFit.cover, + + // Loading indicator + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + height: 70.h, + width: 70.w, + color: Colors.grey[300], + child: const Center( + child: CircularProgressIndicator( + color: Color(0xffF95F62), + strokeWidth: 2, + ), + ), + ); + }, + + // Error UI + errorBuilder: (context, error, stackTrace) { + return Container( + height: 70.h, + width: 70.w, + color: Colors.grey[300], + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + ), + ); + }, + ), + ), + const SizedBox(width: 20), + + // Postcard Details + Expanded( + child: SizedBox( + height: 60.h, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + postcard.pcTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 16.sp, + ), + ), + const SizedBox(height: 6), + Text( + "5 Post cards", + style: GoogleFonts.poppins( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 14.sp, + ), + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(13, 7, 13, 7), + decoration: BoxDecoration( + color: _getStatusColor(postcard.orderStatus).withOpacity(0.16), + border: Border.all( + color: _getStatusBorderColor(postcard.orderStatus), + ), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + _getStatusText(postcard.orderStatus), + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 8.54.sp, + ), + ), + ), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MyPostcardPreviewView( + postcard: postcard, + ), + ), + ); + }, + child: Row( + children: [ + Icon( + Icons.remove_red_eye_outlined, + size: 15, + color: const Color(0xffF95F62), + ), + SizedBox(width: 5.w), + Text( + "Preview", + style: TextStyle( + fontWeight: FontWeight.w400, + color: const Color(0xffF95F62), + fontSize: 13.sp, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + } + + Color _getStatusColor(String status) { + switch (status.toLowerCase()) { + case 'pending': + return const Color(0xffFFA500); + case 'processing': + case 'in progress': + return const Color(0xff00FFA6); + case 'shipped': + return const Color(0xff0096FF); + case 'delivered': + return const Color(0xff00C851); + case 'cancelled': + return const Color(0xffFF4444); + default: + return const Color(0xff00FFA6); + } + } + + Color _getStatusBorderColor(String status) { + switch (status.toLowerCase()) { + case 'pending': + return const Color(0xffCC8400); + case 'processing': + case 'in progress': + return const Color(0xff439F6E); + case 'shipped': + return const Color(0xff0078CC); + case 'delivered': + return const Color(0xff00A041); + case 'cancelled': + return const Color(0xffCC0000); + default: + return const Color(0xff439F6E); + } + } + + String _getStatusText(String status) { + switch (status.toLowerCase()) { + case 'pending': + return 'Pending'; + case 'processing': + return 'Processing'; + case 'in progress': + return 'In Progress'; + case 'shipped': + return 'Shipped'; + case 'delivered': + return 'Delivered'; + case 'cancelled': + return 'Cancelled'; + default: + return status; + } + } +} \ No newline at end of file diff --git a/lib/postcard/views/my_postcard_preview_view.dart b/lib/postcard/views/my_postcard_preview_view.dart new file mode 100644 index 0000000..eedc6ca --- /dev/null +++ b/lib/postcard/views/my_postcard_preview_view.dart @@ -0,0 +1,438 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../common_packages/app_bar.dart'; +import '../../common_packages/back_widget.dart'; +import '../models/my_postcard_model.dart'; +import '../../networkApiServices/api_urls.dart'; + +class MyPostcardPreviewView extends StatefulWidget { + final MyPostCard postcard; + + const MyPostcardPreviewView({ + super.key, + required this.postcard, + }); + + @override + State createState() => _MyPostcardPreviewViewState(); +} + +class _MyPostcardPreviewViewState extends State { + bool showBack = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + backWidget(context, "Preview", Colors.black), + + 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}", + 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: 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(height: 20.h), + + // Flip buttons + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + setState(() { + showBack = false; + }); + }, + child: Row( + children: [ + Icon( + Icons.arrow_back, + color: !showBack ? const Color(0xffF95F62) : Colors.grey[400], + size: 20, + ), + SizedBox(width: 6.w), + Text( + 'Flip', + style: GoogleFonts.poppins( + color: !showBack ? const Color(0xffF95F62) : Colors.grey[400], + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () { + setState(() { + showBack = true; + }); + }, + child: Row( + children: [ + Text( + 'Flip', + style: GoogleFonts.poppins( + color: showBack ? const Color(0xffF95F62) : Colors.grey[400], + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 6.w), + Icon( + Icons.arrow_forward, + color: showBack ? const Color(0xffF95F62) : Colors.grey[400], + size: 20, + ), + ], + ), + ), + ], + ), + ), + + // Postcard Display + Expanded( + child: Center( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + child: showBack ? _buildBackSide() : _buildFrontSide(), + ), + ), + ), + SizedBox(height: 40.h), + ], + ), + ), + ), + ); + } + + Widget _buildFrontSide() { + return Container( + key: const ValueKey('front'), + margin: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: AspectRatio( + aspectRatio: 1.5, // Standard postcard ratio + child: Image.network( + '${ApiUrls.baseUrl}${widget.postcard.pcImagePath}', + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: Colors.grey[300], + child: Center( + child: CircularProgressIndicator( + color: const Color(0xffF95F62), + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[300], + child: const Center( + child: Icon( + Icons.image_not_supported, + size: 60, + color: Colors.grey, + ), + ), + ); + }, + ), + ), + ), + ); + } + + Widget _buildBackSide() { + return Container( + key: const ValueKey('back'), + margin: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xffE2D6C2), + Color(0xffFFF5E6), + Color(0xffFFF5E6), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: const Color(0xff000000).withOpacity(0.12), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: AspectRatio( + aspectRatio: 1.5, + child: Row( + children: [ + // ================= LEFT SIDE ================= + Expanded( + flex: 55, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Logo + Image.asset( + 'assets/logo/logo_city_cards.png', + height: 24.h, // adjust as needed + fit: BoxFit.contain, + ), + SizedBox(height: 2.h), + Text( + 'POSTCARD', + style: TextStyle( + color: Colors.black45, + fontSize: 6.sp, + letterSpacing: 1.4, + fontWeight: FontWeight.w500, + ), + ), + + SizedBox(height: 14.h), + + // Message label + Text( + 'MESSAGE PREVIEW', + style: TextStyle( + color: Colors.black, + fontSize: 6.sp, + letterSpacing: 1.4, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + + // Message text + Expanded( + child: SingleChildScrollView( + child: Text( + widget.postcard.pcContent, + style: TextStyle( + color: Colors.black87, + fontSize: 13.sp, + height: 1.45, + ), + ), + ), + ), + + SizedBox(height: 10.h), + + // Footer + Text( + 'CityCards.co', + style: TextStyle( + color: const Color(0xffF95F62), + fontSize: 12.sp, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + + // ================= DIVIDER ================= + Container( + width: 4, + margin: EdgeInsets.symmetric(vertical: 14.h), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black.withOpacity(0.05), + Colors.black.withOpacity(0.30), + Colors.black.withOpacity(0.05), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + + // ================= RIGHT SIDE ================= + Expanded( + flex: 45, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h), + child: Column( + children: [ + const Spacer(), + + // Address with BORDER + Container( + width: double.infinity, + padding: EdgeInsets.all(4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Colors.black.withOpacity(0.15), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // ADDRESS label + Align( + alignment: Alignment.centerLeft, + child: Text( + 'ADDRESS', + style: TextStyle( + color: Colors.black45, + fontSize: 7.5.sp, + letterSpacing: 1.6, + fontWeight: FontWeight.w600, + ), + ), + ), + + SizedBox(height: 6.h), + + // Address line 1 + Text( + '${widget.postcard.cityName},', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black87, + fontSize: 13.sp, + height: 1.5, + ), + ), + + SizedBox(height: 6.h), + + // State + Text( + '${widget.postcard.stateName},', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black87, + fontSize: 13.sp, + height: 1.5, + ), + ), + SizedBox(height: 6.h), + // Country + Text( + widget.postcard.countryName, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black87, + fontSize: 13.sp, + height: 1.5, + ), + ), + ], + ), + ), + + const Spacer(), + ], + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/postcard/views/my_postcards_view.dart b/lib/postcard/views/my_postcards_view.dart new file mode 100644 index 0000000..059ca67 --- /dev/null +++ b/lib/postcard/views/my_postcards_view.dart @@ -0,0 +1,491 @@ +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:citycards_customer/common_packages/app_bar.dart'; +import 'dart:developer' as developer; +import '../../core/route_constants.dart'; +import '../../login/view/login_email_bottomsheet.dart'; +import '../blocs/myPostCards/my_postcard_bloc.dart'; +import '../blocs/myPostCards/my_postcard_event.dart'; +import '../blocs/myPostCards/my_postcard_state.dart'; +import 'my_postcard_drafts_view.dart'; +import 'my_postcard_orders_view.dart'; + +class MyPostCardsView extends StatefulWidget { + const MyPostCardsView({super.key}); + + @override + State createState() => _MyPostCardsViewState(); +} + +class _MyPostCardsViewState extends State { + bool showDrafts = true; + + @override + void initState() { + super.initState(); + developer.log('🚀 MyPostCardsView initialized', name: 'MyPostCardsView'); + context.read().add(const CheckLoginStatus()); + } + + void _switchTab(bool isDrafts) { + setState(() { + showDrafts = isDrafts; + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: BlocBuilder( + builder: (context, state) { + developer.log('📊 Current state: ${state.runtimeType}', name: 'MyPostCardsView'); + + // Handle not logged in state + if (state is MyPostCardNotLoggedIn) { + developer.log('❌ Showing login page - user not logged in', name: 'MyPostCardsView'); + return _buildPleaseLoginPageUI(); + } + + // Handle checking login state + if (state is MyPostCardCheckingLogin) { + developer.log('🔍 Checking login...', name: 'MyPostCardsView'); + return const Center(child: CircularProgressIndicator()); + } + + // Handle loaded state + if (state is MyPostCardLoaded) { + final isDraftsEmpty = state.draftPostCards.isEmpty; + final isOrdersEmpty = state.orderPostCards.isEmpty; + + developer.log('📊 Loaded - Drafts: ${state.draftPostCards.length}, Orders: ${state.orderPostCards.length}', name: 'MyPostCardsView'); + developer.log('🔄 Loading - Drafts: ${state.isDraftLoading}, Orders: ${state.isOrderLoading}', name: 'MyPostCardsView'); + + // Show initial UI only when both are empty AND not loading + final shouldShowInitialUI = isDraftsEmpty && + isOrdersEmpty && + !state.isDraftLoading && + !state.isOrderLoading; + + if (shouldShowInitialUI) { + developer.log('🎨 Showing initial UI - both lists empty', name: 'MyPostCardsView'); + return _buildInitialPageUI(); + } + + // Show loading state while initial data is being fetched + if (state.isDraftLoading && state.isOrderLoading && + isDraftsEmpty && isOrdersEmpty) { + developer.log('âŗ Showing loading - fetching initial data', name: 'MyPostCardsView'); + return Column( + children: [ + const CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + const Expanded( + child: Center(child: CircularProgressIndicator()), + ), + ], + ); + } + + developer.log('📱 Showing main content UI', name: 'MyPostCardsView'); + return _buildMainContentUI(state); + } + + // Handle error state + if (state is MyPostCardError) { + developer.log('❌ Error state: ${state.errorMessage}', name: 'MyPostCardsView'); + return _buildErrorUI(state.errorMessage); + } + + // Default fallback + developer.log('âš ī¸ Unknown state - showing loading', name: 'MyPostCardsView'); + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ); + } + + Widget _buildMainContentUI(MyPostCardLoaded state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + const CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + + // Tab Buttons + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => _switchTab(true), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: showDrafts + ? const Color(0xffF95F62).withOpacity(0.24) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: showDrafts + ? const Color(0xffF95F62).withOpacity(0.4) + : const Color(0xffE0E0E0), + ), + ), + child: Center( + child: Text( + "My drafts", + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14.sp, + color: showDrafts + ? Colors.black + : Colors.black.withOpacity(0.56), + ), + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () => _switchTab(false), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: !showDrafts + ? const Color(0xffF95F62).withOpacity(0.24) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: !showDrafts + ? const Color(0xffF95F62).withOpacity(0.4) + : const Color(0xffE0E0E0), + ), + ), + child: Center( + child: Text( + "My orders", + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14.sp, + color: !showDrafts + ? Colors.black + : Colors.black.withOpacity(0.56), + ), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 24), + + // Content based on selected tab + Expanded( + child: showDrafts + ? (state.isDraftLoading && state.draftPostCards.isEmpty + ? const Center(child: CircularProgressIndicator()) + : const MyPostCardDraftView()) + : (state.isOrderLoading && state.orderPostCards.isEmpty + ? const Center(child: CircularProgressIndicator()) + : const MyPostCardOrdersView()), + ), + + // Create postcard button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.of(context) + .pushNamed(RouteConstants.uploadPhotoPage); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Create post card", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + } + + // Initial page UI when both lists are empty + Widget _buildInitialPageUI() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + const CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 30.h), + + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + "assets/images/post_card_intro.png", + width: double.infinity, + fit: BoxFit.cover, + ), + ), + + SizedBox(height: 50.h), + + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("🌴", style: TextStyle(fontSize: 16)), + SizedBox(width: 4), + Text("📮", style: TextStyle(fontSize: 16)), + SizedBox(width: 4), + Text("💌", style: TextStyle(fontSize: 16)), + ], + ), + + SizedBox(height: 24.h), + + Text( + "Make the most of your trip", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w800, + color: const Color(0xffF95F62), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8.h), + Text( + "Design your own unique postcards to\ncherish your unforgettable moments.", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff707070), + height: 1.5, + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: 36.h), + ], + ), + ), + ), + + // Create postcard button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pushNamed(RouteConstants.uploadPhotoPage); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Lets Create", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + } + + // Please login page UI when user is not logged in + Widget _buildPleaseLoginPageUI() { + developer.log('🔐 Building login page UI', name: 'MyPostCardsView'); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + const CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 50.h), + + // Postcard Image with opacity + Opacity( + opacity: 0.3, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + "assets/images/post_card_intro.png", + width: double.infinity, + fit: BoxFit.cover, + ), + ), + ), + + SizedBox(height: 60.h), + + // Error Message + Text( + "You are not logged in yet!", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w800, + color: const Color(0xffF95F62), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 12.h), + Text( + "To design your own unique postcards, log\nin and purchase an unlimited pass", + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff707070), + height: 1.5, + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: 36.h), + ], + ), + ), + ), + + // Login button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => const LoginEmailBottomsheet(), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Login", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + } + + // Error UI + Widget _buildErrorUI(String errorMessage) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: Color(0xffF95F62), + ), + SizedBox(height: 16.h), + Text( + "Something went wrong", + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.h), + Text( + errorMessage, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 24.h), + ElevatedButton( + onPressed: () { + context.read().add(const CheckLoginStatus()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + ), + child: const Text("Retry"), + ), + ], + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/postcard/views/postcard_checkout_page_view.dart b/lib/postcard/views/postcard_checkout_page_view.dart index c199a3e..04242fc 100644 --- a/lib/postcard/views/postcard_checkout_page_view.dart +++ b/lib/postcard/views/postcard_checkout_page_view.dart @@ -1,153 +1,547 @@ +import 'dart:io'; +import 'package:citycards_customer/postcard/views/my_postcards_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../StripePayment/bloc/stripe_payment_bloc.dart'; +import '../../StripePayment/bloc/stripe_payment_event.dart'; +import '../../StripePayment/bloc/stripe_payment_state.dart'; +import '../../StripePayment/repository/stripe_service.dart'; import '../../common_packages/app_bar.dart'; +import '../../core/route_constants.dart'; +import '../blocs/myPostCards/my_postcard_bloc.dart'; +import '../blocs/myPostCards/my_postcard_event.dart'; +import '../blocs/postcardCheckout/postcard_checkout_bloc.dart'; +import '../blocs/postcardCheckout/postcard_checkout_event.dart'; +import '../blocs/postcardCheckout/postcard_checkout_state.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_events.dart'; import '../blocs/postcard_creation_state.dart'; import '../widgets/message_card_widget.dart'; import '../widgets/postcard_preview_widget.dart'; -class PostcardCheckoutPageView extends StatelessWidget { - const PostcardCheckoutPageView({super.key}); +class PostcardCheckoutPageView extends StatefulWidget { + final String countryName; + final String cityName; + final String stateName; + final String zipCode; + final String address1; + final String address2; + final String pcTitle; + final String pcNumber; + final String pcDatetime; + final String fullname; + final String emailAddress; + final String mobileNumber; + final String isdCode; + final bool isForSelf; + final double baseAmount; + final double totalTaxAmount; + final double totalAmount; + + const PostcardCheckoutPageView({ + super.key, + required this.countryName, + required this.cityName, + required this.stateName, + required this.zipCode, + this.address1 = '', + this.address2 = '', + required this.pcTitle, + required this.pcNumber, + required this.pcDatetime, + required this.fullname, + required this.emailAddress, + required this.mobileNumber, + required this.isdCode, + required this.isForSelf, + required this.baseAmount, + required this.totalTaxAmount, + required this.totalAmount, + }); + + @override + State createState() => _PostcardCheckoutPageViewState(); +} + +class _PostcardCheckoutPageViewState extends State { + @override + void initState() { + super.initState(); + // Initialize checkout bloc with data from widget + WidgetsBinding.instance.addPostFrameCallback((_) { + final creationState = context.read().state; + + // ⭐ Convert image path to File object + File? imageFile; + if (creationState.imagePath != null && creationState.imagePath!.isNotEmpty) { + imageFile = File(creationState.imagePath!); + } + + context.read().add( + UpdateCheckoutDataEvent( + countryName: widget.countryName, + cityName: widget.cityName, + stateName: widget.stateName, + zipCode: widget.zipCode, + address1: widget.address1, + address2: widget.address2, + pcTitle: widget.pcTitle, + pcContent: creationState.message ?? '', + pcImageFile: imageFile, + pcNumber: widget.pcNumber, + pcDatetime: widget.pcDatetime, + fullname: widget.fullname, + emailAddress: widget.emailAddress, + mobileNumber: widget.mobileNumber, + isdCode: widget.isdCode, + isForSelf: widget.isForSelf, + baseAmount: widget.baseAmount, + totalTaxAmount: widget.totalTaxAmount, + totalAmount: widget.totalAmount, + ), + ); + }); + } + + /// 🆕 Handle payment flow with client secret + Future _handlePaymentFlow(BuildContext context, String clientSecret) async { + // Show payment bottom sheet with BLoC + final paymentSuccess = await showModalBottomSheet( + context: context, + isDismissible: false, + enableDrag: false, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (bottomSheetContext) { + return BlocProvider( + create: (_) => StripePaymentBloc(stripeService: StripeService()) + ..add(InitiatePaymentWithClientSecret(clientSecret: clientSecret)), + child: BlocConsumer( + listener: (context, state) { + if (state is StripePaymentSuccess) { + Navigator.of(bottomSheetContext).pop(true); + } else if (state is StripePaymentFailure || state is StripePaymentCancelled) { + Navigator.of(bottomSheetContext).pop(false); + } + }, + builder: (context, state) { + return Container( + height: MediaQuery.of(context).size.height * 0.5, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (state is StripePaymentLoading) ...[ + const CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Color(0xFFF95F62), + ), + ), + const SizedBox(height: 24), + const Text( + "Processing payment...", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF333333), + ), + ), + ] else if (state is StripePaymentSuccess) ...[ + const Icon( + Icons.check_circle, + color: Colors.green, + size: 64, + ), + const SizedBox(height: 16), + const Text( + "Payment Successful!", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + ), + ] else if (state is StripePaymentFailure) ...[ + const Icon( + Icons.error, + color: Colors.red, + size: 64, + ), + const SizedBox(height: 16), + const Text( + "Payment Failed", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 8), + Text( + state.error, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ] else if (state is StripePaymentCancelled) ...[ + const Icon( + Icons.cancel, + color: Colors.orange, + size: 64, + ), + const SizedBox(height: 16), + const Text( + "Payment Cancelled", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Color(0xFF333333), + ), + ), + ], + const SizedBox(height: 32), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE0E0E0), + ), + ), + child: Column( + children: [ + Text( + "Payment Amount", + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + "\$${widget.totalAmount.toStringAsFixed(2)}", + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.lock_outline, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 6), + Text( + "Secured by Stripe", + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ), + ); + }, + ); + + // Handle payment result + if (!mounted) return; + + if (paymentSuccess == true) { + // Payment successful - continue to next step + context.read().add(GoToNextStep()); + final bloc = context.read(); + bloc.add( + ConfirmPaymentEvent( + stripeStatus: 'succeeded', + paymentStatus: 'success', + ), + ); + } else { + + // Payment failed or cancelled - go to MyPostCardsView + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const MyPostCardsView(), + ), + ); + + final bloc = context.read(); + bloc.add( + ConfirmPaymentEvent( + stripeStatus: 'requires_payment_method', + paymentStatus: 'failed', + ), + ); + } + } @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final bloc = context.read(); - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - // Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Checkout", - style: GoogleFonts.poppins( - fontSize: 20.sp, - fontWeight: FontWeight.w600, - color: const Color(0xff1A1A1A), - ), - ), - TextButton( - onPressed: () { - // TODO: Save as draft - }, - child: Text( - "Save as draft", - style: GoogleFonts.poppins( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: const Color(0xffF95F62), - ), - ), - ), - ], - ), - - const SizedBox(height: 16), - - MessageCardWidget( - message: state.message ?? "", - selectedFont: state.selectedFont, - ), - SizedBox(height: 10.h), - PostCardPreviewWidget( - imagePath: state.imagePath ?? "", - message: state.message ?? "", - selectedFont: state.selectedFont, - ), - - SizedBox(height: 60.h), - - // 💰 Payment Summary - Text( - "Payment summary", - style: TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w400, - color: const Color(0xff999999), - ), - ), - Divider(color: Color(0xffEDEDED)), - const SizedBox(height: 5), - - _buildPaymentRow("Subtotal", "\$ 50"), - const SizedBox(height: 20), - _buildPaymentRow("Discount", "\$ 20", highlight: true), - const SizedBox(height: 8), - Divider(color: Colors.black), - _buildPaymentRow("Grand Total", "\$ 30", size: 20.sp), - const SizedBox(height: 28), - Container(color: Color(0xffFAFAFA), height: 10), - const SizedBox(height: 10), - Row( - children: [ - const Icon( - Icons.home_outlined, - color: Color(0xffF95F62), - size: 20, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - "Unit 7, Level 3, Dummy Towers 33.......", - style: GoogleFonts.poppins( - fontSize: 13.sp, - color: const Color(0xff2D3134), - ), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.edit_outlined, - color: Color(0xffF95F62), - size: 18, - ), - ), - ], - ), - const SizedBox(height: 10), - Container(color: Color(0xffFAFAFA), height: 10), - - const SizedBox(height: 40), - - // 🧾 Pay Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - bloc.add(GoToNextStep()); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), - ), - ), - child: Text( - "Pay \$30", - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], + return BlocConsumer( + listener: (context, checkoutState) { + if (checkoutState.isSuccess && !checkoutState.isDraft) { + // 🆕 Payment flow: Check if we have clientSecret + if (checkoutState.clientSecret != null && checkoutState.clientSecret!.isNotEmpty) { + // Initiate Stripe payment with clientSecret + _handlePaymentFlow(context, checkoutState.clientSecret!); + } else { + // No clientSecret - show error + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error: Payment initialization failed'), + backgroundColor: Colors.red, + ), + ); + // Navigate to MyPostCardsView on error + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const MyPostCardsView(), + ), + ); + } + } else if (checkoutState.isSuccess && checkoutState.isDraft) { + // Draft saved successfully + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Draft saved successfully!'), + backgroundColor: Colors.green, ), - ), + ); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const MyPostCardsView(), + ), + ); + } else if (checkoutState.error != null) { + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${checkoutState.error}'), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, checkoutState) { + return BlocBuilder( + builder: (context, creationState) { + return Stack( + children: [ + SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, + ), + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Checkout", + style: GoogleFonts.poppins( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + TextButton( + onPressed: checkoutState.isLoading + ? null + : () { + context + .read() + .add(SaveAsDraftEvent()); + }, + child: Text( + "Save as draft", + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: checkoutState.isLoading + ? Colors.grey + : const Color(0xffF95F62), + ), + ), + ), + ], + ), + + const SizedBox(height: 16), + + MessageCardWidget( + message: creationState.message ?? "", + selectedFont: creationState.selectedFont, + ), + SizedBox(height: 10.h), + PostCardPreviewWidget( + imagePath: creationState.imagePath ?? "", + message: creationState.message ?? "", + selectedFont: creationState.selectedFont, + ), + + SizedBox(height: 60.h), + + // 💰 Payment Summary + Text( + "Payment summary", + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w400, + color: const Color(0xff999999), + ), + ), + Divider(color: Color(0xffEDEDED)), + const SizedBox(height: 5), + + _buildPaymentRow( + "Subtotal", "\$ ${widget.baseAmount.toStringAsFixed(2)}"), + const SizedBox(height: 20), + _buildPaymentRow( + "Tax", "\$ ${widget.totalTaxAmount.toStringAsFixed(2)}", + highlight: true), + const SizedBox(height: 8), + Divider(color: Colors.black), + _buildPaymentRow( + "Grand Total", "\$ ${widget.totalAmount.toStringAsFixed(2)}", + size: 20.sp), + const SizedBox(height: 28), + Container(color: Color(0xffFAFAFA), height: 10), + const SizedBox(height: 10), + Row( + children: [ + const Icon( + Icons.home_outlined, + color: Color(0xffF95F62), + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + "${widget.cityName}, ${widget.stateName}, ${widget.countryName} ${widget.zipCode}", + style: GoogleFonts.poppins( + fontSize: 13.sp, + color: const Color(0xff2D3134), + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + IconButton( + onPressed: () { + + }, + icon: const Icon( + Icons.edit_outlined, + color: Color(0xffF95F62), + size: 18, + ), + ), + ], + ), + const SizedBox(height: 10), + Container(color: Color(0xffFAFAFA), height: 10), + + const SizedBox(height: 40), + + // 🧾 Pay Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: checkoutState.isLoading + ? null + : () { + context + .read() + .add(SubmitPostcardEvent()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: checkoutState.isLoading + ? SizedBox( + height: 20.h, + width: 20.h, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Text( + "Pay \$${widget.totalAmount.toStringAsFixed(2)}", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + // Loading overlay + if (checkoutState.isLoading) + Container( + color: Colors.black.withOpacity(0.3), + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Color(0xffF95F62)), + ), + ), + ), + ], + ); + }, ); }, ); @@ -155,11 +549,11 @@ class PostcardCheckoutPageView extends StatelessWidget { /// đŸ’ĩ Helper for payment summary row Widget _buildPaymentRow( - String label, - String value, { - bool highlight = false, - double? size, - }) { + String label, + String value, { + bool highlight = false, + double? size, + }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -174,10 +568,10 @@ class PostcardCheckoutPageView extends StatelessWidget { Container( decoration: highlight ? BoxDecoration( - color: const Color(0xffFDCDCE), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Color(0xffEDEDED)), - ) + color: const Color(0xffFDCDCE), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Color(0xffEDEDED)), + ) : null, padding: EdgeInsets.symmetric( horizontal: highlight ? 6 : 0, @@ -195,4 +589,4 @@ class PostcardCheckoutPageView extends StatelessWidget { ], ); } -} +} \ No newline at end of file diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart index 8f3ffc5..cfcb88c 100644 --- a/lib/postcard/views/postcard_creation_page_view.dart +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -8,9 +8,11 @@ import 'package:citycards_customer/postcard/views/write_message_step_page_view.d import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../blocs/postcardCheckout/postcard_checkout_bloc.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_state.dart'; -import 'my_orders_page_view.dart'; +import '../repository/postcard_checkout_repository.dart'; +import 'my_postcards_view.dart'; import 'order_success_page_view.dart'; class PostcardCreationPage extends StatelessWidget { @@ -40,13 +42,35 @@ class PostcardCreationPage extends StatelessWidget { stepWidget = const PostcardPurchaseFormPageView(); break; case PostcardStep.checkout: - stepWidget = const PostcardCheckoutPageView(); + stepWidget = BlocProvider( + create: (_) => PostcardCheckoutBloc( + repository: CreatePostCardRepository(), + ), + child: PostcardCheckoutPageView( + countryName: state.country ?? 'N/A', + cityName: state.city ?? 'N/A', + stateName: state.state ?? 'N/A', + zipCode: state.zipCode ?? 'N/A', + pcTitle: state.pcTitle ?? 'N/A', + pcNumber: '12', + pcDatetime: '2008-11-20', + fullname: state.fullName ?? 'N/A', + emailAddress: state.emailId ?? 'N/A', + mobileNumber: state.phoneNumber ?? 'N/A', + isdCode: '+91', + isForSelf: !state.isGift, + totalTaxAmount: 0.5, + baseAmount: 10, + totalAmount: 10.5, + ), + ); break; + case PostcardStep.orderSuccess: stepWidget = const OrderSuccessPageView(); break; case PostcardStep.myOrders: - stepWidget = const MyOrdersPageView(); + stepWidget = const MyPostCardsView(); break; case PostcardStep.myOrderPostcardPreview: stepWidget = const OrderPostcardPreviewPageView(); diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart index 92d77e2..88e67d0 100644 --- a/lib/postcard/views/postcard_purchase_form_page_view.dart +++ b/lib/postcard/views/postcard_purchase_form_page_view.dart @@ -8,9 +8,38 @@ import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_events.dart'; import '../blocs/postcard_creation_state.dart'; -class PostcardPurchaseFormPageView extends StatelessWidget { +class PostcardPurchaseFormPageView extends StatefulWidget { const PostcardPurchaseFormPageView({super.key}); + @override + State createState() => _PostcardPurchaseFormPageViewState(); +} + +class _PostcardPurchaseFormPageViewState extends State { + final _formKey = GlobalKey(); + + // Controllers + final _titleController = TextEditingController(); + final _fullNameController = TextEditingController(); + final _emailController = TextEditingController(); + final _phoneController = TextEditingController(); + final _cityController = TextEditingController(); + final _zipCodeController = TextEditingController(); + + String? _selectedCountry; + String? _selectedState; + + @override + void dispose() { + _titleController.dispose(); + _fullNameController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + _cityController.dispose(); + _zipCodeController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -20,140 +49,198 @@ class PostcardPurchaseFormPageView extends StatelessWidget { return SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - - // Order ID - Text( - "#78895436", - style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.w600, - color: const Color(0xff1A1A1A), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, ), - ), - const SizedBox(height: 20), - // Postcard image + title - 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)), - ), + // Order ID + Text( + "#78895436", + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), ), - const SizedBox(width: 16), - Expanded( - child: TextField( - 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), - ), + ), + const SizedBox(height: 20), + + // Postcard image + title + 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)), ), - style: GoogleFonts.poppins(fontSize: 14.sp), - onChanged: (val) { - // You can dispatch event here: bloc.add(UpdateTitle(val)); - }, ), + 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), + ), + ), + 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( + "Add personal details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), ), - ], - ), - - const SizedBox(height: 28), - - // Personal details section - Text( - "Add personal details", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: const Color(0xff1A1A1A), ), - ), - const SizedBox(height: 16), + const SizedBox(height: 16), - _buildInputField( - label: "Full Name", - hint: "Lorem Ipsum", - ), - _buildInputField( - label: "Email ID", - hint: "Lorem@gmail.com", - icon: Icons.email_outlined, - ), - _buildInputField( - label: "Phone number", - hint: "+91 9999 999 999", - icon: Icons.phone_outlined, - ), - - const SizedBox(height: 28), - - // Address details section - Text( - "Add address details", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: const Color(0xff1A1A1A), + _buildInputField( + label: "Full Name", + hint: "Lorem Ipsum", + controller: _fullNameController, + ), + _buildInputField( + label: "Email ID", + hint: "Lorem@gmail.com", + icon: Icons.email_outlined, + controller: _emailController, + keyboardType: TextInputType.emailAddress, + ), + _buildInputField( + label: "Phone number", + hint: "+91 9999 999 999", + icon: Icons.phone_outlined, + controller: _phoneController, + keyboardType: TextInputType.phone, ), - ), - const SizedBox(height: 16), - _buildInputField(label: "City", hint: "Lorem Ipsum"), - _buildDropdownField(label: "Country", hint: "Lorem Ipsum"), - _buildDropdownField(label: "State", hint: "Lorem Ipsum"), - _buildInputField(label: "Zip Code", hint: "000000"), + const SizedBox(height: 28), - const SizedBox(height: 30), + // Address details section + Text( + "Add address details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: const Color(0xff1A1A1A), + ), + ), + const SizedBox(height: 16), - // Next Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - bloc.add(GoToNextStep()); + _buildInputField( + label: "City", + hint: "Lorem Ipsum", + controller: _cityController, + ), + _buildDropdownField( + label: "Country", + hint: "Lorem Ipsum", + value: _selectedCountry, + onChanged: (val) { + setState(() { + _selectedCountry = val; + }); }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), + ), + _buildDropdownField( + label: "State", + hint: "Lorem Ipsum", + value: _selectedState, + onChanged: (val) { + setState(() { + _selectedState = val; + }); + }, + ), + _buildInputField( + label: "Zip Code", + hint: "000000", + controller: _zipCodeController, + keyboardType: TextInputType.number, + ), + + 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, + 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, + child: Text( + "Next", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), ), ), ), - ), - ], + ], + ), ), ), ); @@ -165,7 +252,9 @@ class PostcardPurchaseFormPageView extends StatelessWidget { Widget _buildInputField({ required String label, required String hint, + required TextEditingController controller, IconData? icon, + TextInputType? keyboardType, }) { return Padding( padding: const EdgeInsets.only(bottom: 18), @@ -181,7 +270,9 @@ class PostcardPurchaseFormPageView extends StatelessWidget { ), ), const SizedBox(height: 6), - TextField( + TextFormField( + controller: controller, + keyboardType: keyboardType, decoration: InputDecoration( hintText: hint, hintStyle: GoogleFonts.poppins( @@ -201,7 +292,24 @@ class PostcardPurchaseFormPageView extends StatelessWidget { borderSide: const BorderSide(color: Color(0xffFDCDCE)), borderRadius: BorderRadius.circular(8), ), + errorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter $label'; + } + if (label == "Email ID" && !value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, ), ], ), @@ -212,6 +320,8 @@ class PostcardPurchaseFormPageView extends StatelessWidget { Widget _buildDropdownField({ required String label, required String hint, + required String? value, + required Function(String?) onChanged, }) { return Padding( padding: const EdgeInsets.only(bottom: 18), @@ -228,7 +338,7 @@ class PostcardPurchaseFormPageView extends StatelessWidget { ), const SizedBox(height: 6), DropdownButtonFormField( - value: null, + value: value, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), @@ -240,6 +350,14 @@ class PostcardPurchaseFormPageView extends StatelessWidget { borderSide: const BorderSide(color: Color(0xffFDCDCE)), borderRadius: BorderRadius.circular(8), ), + errorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), ), icon: const Icon(Icons.keyboard_arrow_down, color: Color(0xffFDCDCE)), @@ -251,12 +369,20 @@ class PostcardPurchaseFormPageView extends StatelessWidget { ), ), items: const [ - DropdownMenuItem(value: "Lorem Ipsum", child: Text("Lorem Ipsum")), + DropdownMenuItem(value: "India", child: Text("India")), + DropdownMenuItem(value: "USA", child: Text("USA")), + // Add more items as needed ], - onChanged: (val) {}, + onChanged: onChanged, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select $label'; + } + return null; + }, ), ], ), ); } -} +} \ No newline at end of file diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart index 39f41da..9f3f696 100644 --- a/lib/postcard/widgets/purchase_details_bottom_sheet.dart +++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../checkout/bloc/pass_purchase_details_bloc.dart'; +import '../../checkout/bloc/pass_purchase_details_event.dart'; +import '../../checkout/bloc/pass_purchase_details_state.dart'; +import '../../profile/view/edit_profile/edit_profile_view.dart'; import '../blocs/postcard_creation_bloc.dart'; import '../blocs/postcard_creation_events.dart'; import '../blocs/postcard_creation_state.dart'; - class PurchaseDetailsBottomSheet { static void show(BuildContext context) { final existingBloc = BlocProvider.of(context); @@ -17,189 +20,230 @@ class PurchaseDetailsBottomSheet { borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (BuildContext modalContext) { - return BlocProvider.value( - value: existingBloc, + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: existingBloc), + BlocProvider( + create: (_) => PurchaseDetailsBloc()..add(LoadProfileEvent()), + ), + ], child: BlocBuilder( - builder: (context, state) { - final bloc = context.read(); + builder: (context, postcardState) { + final postcardBloc = context.read(); - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - top: 16, - left: 16, - right: 16, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 45, - height: 5, - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(10), - ), + return BlocBuilder( + builder: (context, purchaseState) { + final purchaseBloc = context.read(); + + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + top: 16, + left: 16, + right: 16, ), - const SizedBox(height: 12), - - Text( - "Purchase Details", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 24), - - // đŸŸĨ Option 1: Buy Postcard for Myself - GestureDetector( - onTap: () => bloc.add(TogglePurchaseOption(false)), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: !state.isGift - ? const Color(0xffF95F62) - : const Color(0xffE0E0E0), - width: 1.5, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 45, + height: 5, + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(10), ), ), - child: Row( - children: [ - Radio( - value: false, - groupValue: state.isGift, - onChanged: (_) => - bloc.add(TogglePurchaseOption(false)), - activeColor: const Color(0xffF95F62), + const SizedBox(height: 12), + + Text( + "Purchase Details", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 24), + + // đŸŸĨ Option 1: Buy Postcard for Myself + GestureDetector( + onTap: () { + postcardBloc.add(TogglePurchaseOption(false)); + purchaseBloc.add(ToggleGiftModeEvent(false)); + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: !postcardState.isGift + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text( - "Buy Postcard for Myself", + child: Row( + children: [ + Radio( + value: false, + groupValue: postcardState.isGift, + onChanged: (_) { + postcardBloc.add(TogglePurchaseOption(false)); + purchaseBloc.add(ToggleGiftModeEvent(false)); + }, + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Buy Postcard for Myself", + style: TextStyle( + fontWeight: FontWeight.w600, + color: !postcardState.isGift + ? const Color(0xffF95F62) + : const Color(0xff9E9E9E), + ), + ), + if (!postcardState.isGift && purchaseState.profile != null) ...[ + const SizedBox(height: 8), + Text( + "${purchaseState.profile!.firstName} ${purchaseState.profile!.lastName}", + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Color(0xff1A1A1A), + ), + ), + Text( + "${purchaseState.profile!.address1 ?? ""}\n${purchaseState.profile!.address2 ?? ""}", + style: const TextStyle( + fontSize: 13, + color: Color(0xff5E5E5E), + ), + ), + ], + if (!postcardState.isGift && purchaseState.isLoadingProfile) ...[ + const SizedBox(height: 8), + const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xffF95F62), + ), + ), + ], + ], + ), + ), + if (!postcardState.isGift) + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const EditProfilePage(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + "Edit Details", + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 20), + + // đŸŠļ Option 2: Gift the Postcard + GestureDetector( + onTap: () { + postcardBloc.add(TogglePurchaseOption(true)); + purchaseBloc.add(ToggleGiftModeEvent(true)); + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: postcardState.isGift + ? const Color(0xffF95F62) + : const Color(0xffE0E0E0), + width: 1.5, + ), + ), + child: Row( + children: [ + Radio( + value: true, + groupValue: postcardState.isGift, + onChanged: (_) { + postcardBloc.add(TogglePurchaseOption(true)); + purchaseBloc.add(ToggleGiftModeEvent(true)); + }, + activeColor: const Color(0xffF95F62), + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + "Gift the Postcard for someone else", style: TextStyle( fontWeight: FontWeight.w600, color: Color(0xffF95F62), ), ), - SizedBox(height: 8), - Text( - "Frank Adam", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Color(0xff1A1A1A), - ), - ), - Text( - "132 My Street, Kingston, NY\n12401", - style: TextStyle( - fontSize: 13, - color: Color(0xff5E5E5E), - ), - ), - ], - ), - ), - ElevatedButton( - onPressed: () { - PurchaseDetailsBottomSheet.close(context); - bloc.add(GoToNextStep()); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: const EdgeInsets.symmetric( - horizontal: 14, vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), ), - ), - child: const Text( - "Edit Details", - style: TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), + ], ), - ], - ), - ), - ), - - const SizedBox(height: 20), - - // đŸŠļ Option 2: Gift the Postcard - GestureDetector( - onTap: () => bloc.add(TogglePurchaseOption(true)), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: state.isGift - ? const Color(0xffF95F62) - : const Color(0xffE0E0E0), - width: 1.5, ), ), - child: Row( - children: [ - Radio( - value: true, - groupValue: state.isGift, - onChanged: (_) => - bloc.add(TogglePurchaseOption(true)), - activeColor: const Color(0xffF95F62), - ), - const SizedBox(width: 8), - const Expanded( - child: Text( - "Gift the Postcard for someone else", - style: TextStyle( - fontWeight: FontWeight.w600, - color: Color(0xffF95F62), - ), + + const SizedBox(height: 15), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + PurchaseDetailsBottomSheet.close(context); + postcardBloc.add(GoToNextStep()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xffF95F62), + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: Text( + "Proceed", + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, ), ), - ], - ), - ), - ), - - const SizedBox(height: 15), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - PurchaseDetailsBottomSheet.close(context); - bloc.add(GoToNextStep()); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xffF95F62), - padding: EdgeInsets.symmetric(vertical: 16.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), ), ), - child: Text( - "Proceed", - style: TextStyle( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, - ), - ), - ), + const SizedBox(height: 15), + ], ), - const SizedBox(height: 15), - ], - ), + ); + }, ); }, ), @@ -211,4 +255,4 @@ class PurchaseDetailsBottomSheet { static void close(BuildContext context) { Navigator.of(context).pop(); } -} +} \ No newline at end of file diff --git a/lib/profile/bloc/contactUs/contact_us_bloc.dart b/lib/profile/bloc/contactUs/contact_us_bloc.dart new file mode 100644 index 0000000..caca266 --- /dev/null +++ b/lib/profile/bloc/contactUs/contact_us_bloc.dart @@ -0,0 +1,42 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/contact_us_repository.dart'; +import 'contact_us_event.dart'; +import 'contact_us_state.dart'; + +class ContactUsBloc extends Bloc { + final ContactUsRepository repository; + + ContactUsBloc({required this.repository}) + : super(ContactUsInitial()) { + on(_onSubmitContactUs); + } + + Future _onSubmitContactUs( + SubmitContactUsEvent event, + Emitter emit, + ) async { + emit(ContactUsLoading()); + + try { + final response = await repository.submitTicket( + firstName: event.firstName, + lastName: event.lastName, + emailAddress: event.emailAddress, + mobileNumber: event.mobileNumber, + description: event.description, + ); + + emit( + ContactUsSuccess( + message: response['message'] ?? 'Ticket submitted successfully', + ), + ); + } catch (e) { + emit( + ContactUsFailure( + error: e.toString().replaceAll('Exception:', '').trim(), + ), + ); + } + } +} diff --git a/lib/profile/bloc/contactUs/contact_us_event.dart b/lib/profile/bloc/contactUs/contact_us_event.dart new file mode 100644 index 0000000..4d31a82 --- /dev/null +++ b/lib/profile/bloc/contactUs/contact_us_event.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +abstract class ContactUsEvent extends Equatable { + const ContactUsEvent(); + + @override + List get props => []; +} + +/// Event to submit contact us / support ticket +class SubmitContactUsEvent extends ContactUsEvent { + final String firstName; + final String lastName; + final String emailAddress; + final String mobileNumber; + final String description; + + const SubmitContactUsEvent({ + required this.firstName, + required this.lastName, + required this.emailAddress, + required this.mobileNumber, + required this.description, + }); + + @override + List get props => [ + firstName, + lastName, + emailAddress, + mobileNumber, + description, + ]; +} diff --git a/lib/profile/bloc/contactUs/contact_us_state.dart b/lib/profile/bloc/contactUs/contact_us_state.dart new file mode 100644 index 0000000..ebfadb0 --- /dev/null +++ b/lib/profile/bloc/contactUs/contact_us_state.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +abstract class ContactUsState extends Equatable { + const ContactUsState(); + + @override + List get props => []; +} + +/// Initial state +class ContactUsInitial extends ContactUsState {} + +/// Loading state while submitting ticket +class ContactUsLoading extends ContactUsState {} + +/// Success state +class ContactUsSuccess extends ContactUsState { + final String message; + + const ContactUsSuccess({required this.message}); + + @override + List get props => [message]; +} + +/// Error state +class ContactUsFailure extends ContactUsState { + final String error; + + const ContactUsFailure({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/profile/bloc/profile/profile_bloc.dart b/lib/profile/bloc/profile/profile_bloc.dart index 97fa946..4a3a6e3 100644 --- a/lib/profile/bloc/profile/profile_bloc.dart +++ b/lib/profile/bloc/profile/profile_bloc.dart @@ -195,8 +195,7 @@ class ProfileBloc extends Bloc { print('📄 [BLOC] LogoutEvent received'); } - // Clear local preferences (uncomment when ready) - // await LocalPreference.clearPreference(); + await LocalPreference.resetAppData(); emit(const ProfileLoggedOut()); emit(const ProfileInitial()); diff --git a/lib/profile/repository/contact_us_repository.dart b/lib/profile/repository/contact_us_repository.dart new file mode 100644 index 0000000..6c5a756 --- /dev/null +++ b/lib/profile/repository/contact_us_repository.dart @@ -0,0 +1,32 @@ +import '../../networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class ContactUsRepository { + final NetworkApiService _apiServices = NetworkApiService(); + + /// Submit support ticket + Future> submitTicket({ + required String firstName, + required String lastName, + required String emailAddress, + required String mobileNumber, + required String description, + }) async { + try { + final response = await _apiServices.postApi( + url: ApiUrls.submitTicket, // add this key in ApiUrls + data: { + "firstName": firstName, + "lastName": lastName, + "emailAddress": emailAddress, + "mobileNumber": mobileNumber, + "description": description, + }, + ); + + return response.data as Map; + } catch (e) { + throw Exception('Failed to submit ticket: $e'); + } + } +} diff --git a/lib/profile/view/contact_us/contact_us_view.dart b/lib/profile/view/contact_us/contact_us_view.dart new file mode 100644 index 0000000..5aa6e41 --- /dev/null +++ b/lib/profile/view/contact_us/contact_us_view.dart @@ -0,0 +1,271 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/back_widget.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/common_packages/custom_textfield.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../bloc/contactUs/contact_us_bloc.dart'; +import '../../bloc/contactUs/contact_us_event.dart'; +import '../../bloc/contactUs/contact_us_state.dart'; +import '../../repository/contact_us_repository.dart'; + +class ContactUsPage extends StatelessWidget { + const ContactUsPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ContactUsBloc(repository: ContactUsRepository()), + child: const _ContactUsView(), + ); + } +} + +class _ContactUsView extends StatelessWidget { + const _ContactUsView(); + + @override + Widget build(BuildContext context) { + final firstNameController = TextEditingController(); + final lastNameController = TextEditingController(); + final emailController = TextEditingController(); + final phoneController = TextEditingController(); + final messageController = TextEditingController(); + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: BlocListener( + listener: (context, state) { + if (state is ContactUsSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.green, + ), + ); + + firstNameController.clear(); + lastNameController.clear(); + emailController.clear(); + phoneController.clear(); + messageController.clear(); + } + + if (state is ContactUsFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: Colors.red, + ), + ); + } + }, + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: true, + showDivider: true, + ), + + backWidget(context, "Contact Us", Colors.black), + SizedBox(height: 22.h), + + CustomText( + text: + "You can get in touch with us through the below platforms. Our team will contact you shortly", + size: 14.sp, + color: Colors.black.withOpacity(.6), + ), + SizedBox(height: 20.h), + + /// Customer Support Section + Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 16.h, + ), + decoration: BoxDecoration( + color: const Color(0x00000005).withOpacity(.02), + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: "Customer Support", + size: 18.sp, + weight: FontWeight.w500, + ), + SizedBox(height: 16.h), + _supportBox( + icon: Icons.phone, + title: "Contact Number", + subtitle: "+1012 3456 789", + action: "Tap to call", + ), + SizedBox(height: 12.h), + _supportBox( + icon: Icons.email_rounded, + title: "Email", + subtitle: "citycards24@gmail.com", + action: "Tap to email", + ), + SizedBox(height: 12.h), + _supportBox( + icon: Icons.location_on, + title: "Location", + subtitle: + "132 Dartmouth Street Boston, Massachusetts 02156 United States", + action: "View on map", + ), + ], + ), + ), + SizedBox(height: 24.h), + + /// Form Fields + CustomTextField( + label: "First Name", + hint: "Enter your first name", + controller: firstNameController, + ), + CustomTextField( + label: "Last Name", + hint: "Enter your last name", + controller: lastNameController, + ), + CustomTextField( + label: "Email", + hint: "Enter your email address", + controller: emailController, + ), + CustomTextField( + label: "Phone Number", + hint: "Enter your phone number", + controller: phoneController, + ), + CustomTextField( + label: "Description", + hint: "Write your message here", + maxLines: 4, + controller: messageController, + ), + + SizedBox(height: 24.h), + + /// Submit Button with Loading + BlocBuilder( + builder: (context, state) { + final isLoading = state is ContactUsLoading; + + return SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(38.r), + ), + padding: EdgeInsets.symmetric(vertical: 6.h), + ), + onPressed: isLoading + ? null + : () { + context.read().add( + SubmitContactUsEvent( + firstName: firstNameController.text.trim(), + lastName: lastNameController.text.trim(), + emailAddress: emailController.text.trim(), + mobileNumber: phoneController.text.trim(), + description: messageController.text.trim(), + ), + ); + }, + child: isLoading + ? SizedBox( + height: 22.h, + width: 22.h, + child: const CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : CustomText( + text: "Submit Ticket", + size: 16.sp, + weight: FontWeight.w500, + color: Colors.white, + ), + ), + ); + }, + ), + SizedBox(height: 20.h), + ], + ), + ), + ), + ), + ); + } + + /// Support Box Widget + static Widget _supportBox({ + required IconData icon, + required String title, + required String subtitle, + required String action, + }) { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: const Color(0xFFF95F62), width: 0.8), + color: Colors.white, + ), + child: Row( + children: [ + Icon(icon, color: const Color(0xFFF95F62), size: 32.sp), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: title, + size: 11.sp, + weight: FontWeight.w600, + color: Colors.black.withOpacity(.6), + ), + SizedBox(height: 6.h), + Text( + subtitle, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 2.h), + Text( + action, + style: TextStyle( + fontSize: 11.sp, + color: Colors.black.withOpacity(.4), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart index 12b8b56..b58ca4c 100644 --- a/lib/profile/view/edit_profile/edit_profile_view.dart +++ b/lib/profile/view/edit_profile/edit_profile_view.dart @@ -46,6 +46,8 @@ class _EditProfilePageState extends State { } final userId = await LocalPreference.getUserId(); + if (!mounted) return; + if (userId != null) { context.read().add(FetchProfileEvent(userId: userId)); } @@ -174,6 +176,8 @@ class _EditProfilePageState extends State { print('đŸ”ĩ [EDIT PROFILE] File size: ${(fileSize / 1024).toStringAsFixed(2)} KB'); } + if (!mounted) return; + // ⭐ REPLACED setState with BLoC event context.read().add( ProfileImageSelectedEvent(imageFile: imageFile), @@ -184,6 +188,8 @@ class _EditProfilePageState extends State { print('❌ [EDIT PROFILE] Error picking image: $e'); } + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to pick image: $e'), @@ -279,6 +285,7 @@ class _EditProfilePageState extends State { final userId = await LocalPreference.getUserId(); if (userId == null) { + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('User ID not found'), @@ -288,6 +295,8 @@ class _EditProfilePageState extends State { return; } + if (!mounted) return; + // ⭐ Get selectedImageFile from current BLoC state File? imageFileToSend; final currentState = context.read().state; @@ -333,8 +342,18 @@ class _EditProfilePageState extends State { backgroundColor: Colors.white, body: SafeArea( child: BlocConsumer( - listener: (context, state) { + listener: (context, state) async { if (state is ProfileUpdated) { + if (state.profile.profileImage != null && + state.profile.profileImage!.isNotEmpty) { + await LocalPreference.setProfileImage( + state.profile.profileImage!, + ); + } + + // Check if widget is still mounted before using context + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), diff --git a/lib/profile/view/profile_page_view.dart b/lib/profile/view/profile_page_view.dart index 0fafa63..fc0efa3 100644 --- a/lib/profile/view/profile_page_view.dart +++ b/lib/profile/view/profile_page_view.dart @@ -35,7 +35,20 @@ class _ProfilePageState extends State { return Scaffold( backgroundColor: Colors.white, body: SafeArea( - child: BlocBuilder( + child: BlocConsumer( + listener: (context, state) { + // ⭐ SOLUTION: Auto-refresh when profile is updated (from edit page) + // This prevents race conditions with manual navigation callbacks + if (state is ProfileUpdated) { + // Profile was just updated, fetch fresh data + final userId = state.profile.id; + if (mounted) { + context.read().add( + FetchProfileEvent(userId: userId), + ); + } + } + }, builder: (context, state) { // ⭐ Show loading during initial checks and profile loading if (state is ProfileInitial || @@ -126,6 +139,10 @@ class _ProfilePageState extends State { ElevatedButton( onPressed: () async { final userId = await LocalPreference.getUserId(); + + // ⭐ Check if widget is still mounted after async call + if (!mounted) return; + if (userId != null) { context.read().add( FetchProfileEvent(userId: userId), @@ -213,8 +230,11 @@ class _ProfilePageState extends State { padding: EdgeInsets.symmetric(vertical: 6.h), ), onPressed: () { - // ⭐ REPLACED setState with BLoC event context.read().add(const LogoutEvent()); + Navigator.pushReplacementNamed( + context, + RouteConstants.home, + ); }, child: Text( 'Log out', @@ -451,20 +471,13 @@ class _ProfilePageState extends State { _buildListTile( icon: "assets/icons/user_profile.png", title: 'Edit profile', - onTap: () async { - final result = await Navigator.pushNamed( + onTap: () { + // ⭐ SOLUTION: Just navigate - BlocListener will auto-refresh on ProfileUpdated + // This prevents race conditions from manual refresh callbacks + Navigator.pushNamed( context, RouteConstants.editProfile, ); - - if (result == true) { - final userId = await LocalPreference.getUserId(); - if (userId != null) { - context.read().add( - FetchProfileEvent(userId: userId), - ); - } - } }, ),