diff --git a/assets/images/guest_illustration.png b/assets/images/guest_illustration.png new file mode 100644 index 0000000..e0aa349 Binary files /dev/null and b/assets/images/guest_illustration.png differ diff --git a/assets/images/not_login.png b/assets/images/not_login.png new file mode 100644 index 0000000..7dac2af Binary files /dev/null and b/assets/images/not_login.png differ diff --git a/lib/common_packages/custom_text.dart b/lib/common_packages/custom_text.dart index 6602eea..f0d13e7 100644 --- a/lib/common_packages/custom_text.dart +++ b/lib/common_packages/custom_text.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; class CustomText extends StatelessWidget { @@ -8,6 +7,7 @@ class CustomText extends StatelessWidget { final String text; final int? maxLines; final TextOverflow? overflow; + final TextAlign? textAlign; const CustomText({ Key? key, @@ -17,6 +17,7 @@ class CustomText extends StatelessWidget { required this.text, this.maxLines, this.overflow, + this.textAlign, }) : super(key: key); @override @@ -37,7 +38,7 @@ class CustomText extends StatelessWidget { ), maxLines: maxLines, overflow: overflow, + textAlign: textAlign, ); } -} - +} \ No newline at end of file diff --git a/lib/common_packages/custom_textfield.dart b/lib/common_packages/custom_textfield.dart index 8d4dd79..ac4b67f 100644 --- a/lib/common_packages/custom_textfield.dart +++ b/lib/common_packages/custom_textfield.dart @@ -7,7 +7,12 @@ class CustomTextField extends StatelessWidget { final String hint; final TextEditingController controller; final int? maxLines; - final bool enabled; // ✅ NEW PARAMETER + final bool enabled; + final String? Function(String?)? validator; // ✅ NEW: Validator function + final TextInputType? keyboardType; // ✅ NEW: Keyboard type + final bool obscureText; // ✅ NEW: For password fields + final Widget? suffixIcon; // ✅ NEW: For icons like visibility toggle + final void Function(String)? onChanged; // ✅ NEW: OnChanged callback const CustomTextField({ super.key, @@ -15,7 +20,12 @@ class CustomTextField extends StatelessWidget { required this.hint, required this.controller, this.maxLines = 1, - this.enabled = true, // ✅ default enabled + this.enabled = true, + this.validator, + this.keyboardType, + this.obscureText = false, + this.suffixIcon, + this.onChanged, }); @override @@ -32,10 +42,14 @@ class CustomTextField extends StatelessWidget { SizedBox(height: 6.h), SizedBox( height: maxLines == 1 ? 42.h : null, - child: TextField( + child: TextFormField( // ✅ Changed from TextField to TextFormField controller: controller, - maxLines: maxLines, - enabled: enabled, // ✅ applied here + maxLines: obscureText ? 1 : maxLines, // ✅ Password fields always single line + enabled: enabled, + validator: validator, // ✅ Added validator + keyboardType: keyboardType, // ✅ Added keyboard type + obscureText: obscureText, // ✅ Added obscure text + onChanged: onChanged, // ✅ Added onChanged decoration: InputDecoration( hintText: hint, hintStyle: TextStyle( @@ -45,8 +59,12 @@ class CustomTextField extends StatelessWidget { filled: true, fillColor: enabled ? const Color(0xFFFFF5F5) - : Colors.grey.shade200, // subtle disabled look - contentPadding: EdgeInsets.symmetric(horizontal: 24.w), + : Colors.grey.shade200, + contentPadding: EdgeInsets.symmetric( + horizontal: 24.w, + vertical: maxLines != null && maxLines! > 1 ? 12.h : 0, // ✅ Better padding for multiline + ), + suffixIcon: suffixIcon, // ✅ Added suffix icon enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), borderSide: BorderSide( @@ -68,6 +86,24 @@ class CustomTextField extends StatelessWidget { width: .4.w, ), ), + errorBorder: OutlineInputBorder( // ✅ NEW: Error state border + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: Colors.red, + width: 1.w, + ), + ), + focusedErrorBorder: OutlineInputBorder( // ✅ NEW: Focused error state + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: Colors.red, + width: 1.5.w, + ), + ), + errorStyle: TextStyle( // ✅ NEW: Error text style + fontSize: 11.sp, + color: Colors.red, + ), ), ), ), @@ -75,4 +111,4 @@ class CustomTextField extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 1def1b5..dd0c2c9 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -16,7 +16,7 @@ import 'package:citycards_customer/itinerary_creation/bloc/itinerary_steps_selec import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart'; import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_view.dart'; import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_empty_view.dart'; -import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_filled_view.dart'; +import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart'; import 'package:citycards_customer/offer_pass_detail/offer_pass_detail_view.dart'; import 'package:citycards_customer/search_offers/bloc/search_offers_listing_bloc.dart'; import 'package:citycards_customer/search_offers/view/search_offers_with_listing.dart'; @@ -221,7 +221,7 @@ class AppRouter { case RouteConstants.magicItineraryFilledScreen: return MaterialPageRoute( builder: (_) { - return MagicItineraryFilledView(); + return MagicItineraryView(); }, ); diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart index 73664f6..95017ca 100644 --- a/lib/core/inside_bottom_navigator.dart +++ b/lib/core/inside_bottom_navigator.dart @@ -15,7 +15,7 @@ import '../intro_screens/views/intro_screen_view.dart'; import '../itinerary_creation/bloc/itinerary_detail_bloc.dart'; import '../itinerary_creation/bloc/itinerary_steps_selection_bloc.dart'; import '../itinerary_creation/views/itinerary_creation_view.dart'; -import '../itinerary_creation/views/magic_itinerary_filled_view.dart'; +import '../itinerary_creation/views/magic_itinerary_view.dart'; import '../my_pass/views/booking_page_view.dart'; import '../my_pass/views/booking_successful_page_view.dart'; import '../my_pass/views/qr_pass_page_view.dart'; @@ -155,7 +155,7 @@ Widget buildOffstageNavigator( case RouteConstants.magicItineraryFilledScreen: return MaterialPageRoute(builder: (_){ - return MagicItineraryFilledView(); + return MagicItineraryView(); }); case RouteConstants.checkout: diff --git a/lib/create_account/bloc/create_account_bloc.dart b/lib/create_account/bloc/create_account_bloc.dart index aa6c0e4..6c2abcd 100644 --- a/lib/create_account/bloc/create_account_bloc.dart +++ b/lib/create_account/bloc/create_account_bloc.dart @@ -36,6 +36,15 @@ class CreateAccountBloc extends Bloc { refreshToken: userModel.refreshToken, refreshTokenMaxAge: userModel.refreshTokenMaxAge, ); + await LocalPreference.setUserDetails( + userId: userModel.user.id, + firstName: userModel.user.firstName, + lastName: userModel.user.lastName, + fullName: userModel.user.fullName, + emailAddress: userModel.user.emailAddress, + role: userModel.user.role, + roleId: userModel.user.roleId, + ); emit(CreateAccountSuccess( message: response['message'] ?? 'Account created successfully', userData: response['data'] ?? {}, diff --git a/lib/edit_profile/edit_profile_view.dart b/lib/edit_profile/edit_profile_view.dart index 809cc07..f6b9d2a 100644 --- a/lib/edit_profile/edit_profile_view.dart +++ b/lib/edit_profile/edit_profile_view.dart @@ -4,169 +4,351 @@ import 'package:citycards_customer/common_packages/custom_textfield.dart'; import 'package:flutter/material.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class EditProfilePage extends StatelessWidget { +import '../../localPreference/local_preference.dart'; +import '../profile/bloc/profile/profile_bloc.dart'; +import '../profile/bloc/profile/profile_event.dart'; +import '../profile/bloc/profile/profile_state.dart'; +import '../profile/models/profile_model.dart'; + +class EditProfilePage extends StatefulWidget { const EditProfilePage({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 addressController = TextEditingController(); + State createState() => _EditProfilePageState(); +} - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Header - CommonAppBar(isWhiteLogo: false, isProfilePage: true,showDivider: true,), +class _EditProfilePageState extends State { + // Controllers + final TextEditingController firstNameController = TextEditingController(); + final TextEditingController lastNameController = TextEditingController(); + final TextEditingController phoneController = TextEditingController(); + final TextEditingController address1Controller = TextEditingController(); + final TextEditingController address2Controller = TextEditingController(); - // Back + title - backWidget(context,"Edit Profile", Colors.black), - SizedBox(height: 33.h), + final _formKey = GlobalKey(); - // Profile Image - CircleAvatar( - radius: 38.r, - backgroundImage: AssetImage("assets/images/profile_img.png"), - ), - SizedBox(height: 18.h), - Text( - "Change Profile Picture", - style: TextStyle( - fontSize: 12.sp, - color: Color(0xFFF95F62), - fontWeight: FontWeight.w400, - ), - ), - SizedBox(height: 40.h), + @override + void initState() { + super.initState(); + _fetchProfile(); + } - // Personal Information - Align( - alignment: Alignment.centerLeft, - child: CustomText( - text: "Personal Information", - size: 18.sp, - weight: FontWeight.w500, - ), - ), - SizedBox(height: 12.h), + Future _fetchProfile() async { + final userId = await LocalPreference.getUserId(); + if (userId != null) { + context.read().add(FetchProfileEvent(userId: userId)); + } + } - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "First Name", - hint: "Enter your first name", - controller: firstNameController, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "Last Name", - hint: "Enter your last name", - controller: lastNameController, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "Email", - hint: "Enter your email address", - controller: emailController, - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: CustomTextField( - label: "Phone Number", - hint: "Enter your phone number", - controller: phoneController, - ), - ), + void _populateFields(ProfileModel profile) { + firstNameController.text = profile.firstName; + lastNameController.text = profile.lastName; + phoneController.text = profile.mobileNumber; + address1Controller.text = profile.address1 ?? ''; + address2Controller.text = profile.address2 ?? ''; + } - SizedBox(height: 2.h), - - // Location Details - Align( - alignment: Alignment.centerLeft, - child: CustomText( - text: "Location Details", - size: 18.sp, - weight: FontWeight.w500, - ), - ), - SizedBox(height: 16.h), - - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0.w), - child: CustomTextField( - label: "Address 1", - hint: "Enter address manually or tap to search", - controller: addressController, - ), - ), - - SizedBox(height: 26.h), - - // Buttons - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFFF95F62), - side: const BorderSide(color: Colors.transparent), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(38.r), - ), - padding: EdgeInsets.symmetric(vertical: 12.h), - ), - onPressed: () {}, - child: Text( - "Cancel", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - SizedBox(width: 16.w), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF95F62), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(38.r), - ), - padding: EdgeInsets.symmetric(vertical: 6.h), - ), - onPressed: () {}, - child: Text( - "Save", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - ), - ), - ], - ), - SizedBox(height: 20.h), - ], + void _saveProfile() async { + if (_formKey.currentState?.validate() ?? false) { + final userId = await LocalPreference.getUserId(); + if (userId != null) { + // No setState here - BLoC will handle the state + context.read().add( + UpdateProfileEvent( + userId: userId, + firstName: firstNameController.text.trim(), + lastName: lastNameController.text.trim(), + mobileNumber: phoneController.text.trim(), + address1: address1Controller.text.trim().isEmpty + ? null + : address1Controller.text.trim(), + address2: address2Controller.text.trim().isEmpty + ? null + : address2Controller.text.trim(), ), - ), - ), + ); + } + } + } + + @override + void dispose() { + firstNameController.dispose(); + lastNameController.dispose(); + phoneController.dispose(); + address1Controller.dispose(); + address2Controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is ProfileLoaded) { + _populateFields(state.profile); + } else if (state is ProfileUpdated) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.green, + duration: const Duration(seconds: 2), + ), + ); + // Return true to indicate successful update + Navigator.pop(context, true); + } else if (state is ProfileError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + }, + builder: (context, state) { + // Determine loading state from BLoC + final isLoading = state is ProfileLoading || state is ProfileUpdating; + final isInitialLoading = state is ProfileLoading; + + // Show loading on initial fetch + if (isInitialLoading) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: CircularProgressIndicator( + color: Color(0xFFF95F62), + ), + ), + ); + } + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Stack( + children: [ + SingleChildScrollView( + padding: + EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Header + CommonAppBar( + isWhiteLogo: false, + isProfilePage: true, + showDivider: true, + ), + + // Back + title + backWidget(context, "Edit Profile", Colors.black), + SizedBox(height: 33.h), + + // Personal Details + Align( + alignment: Alignment.centerLeft, + child: CustomText( + text: "Personal Details", + size: 18.sp, + weight: FontWeight.w500, + ), + ), + SizedBox(height: 16.h), + + // First Name + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "First Name", + hint: "Enter your first name", + controller: firstNameController, + enabled: !isLoading, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'First name is required'; + } + return null; + }, + ), + ), + + // Last Name + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Last Name", + hint: "Enter your last name", + controller: lastNameController, + enabled: !isLoading, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Last name is required'; + } + return null; + }, + ), + ), + + // Phone Number + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Phone Number", + hint: "Enter your phone number", + controller: phoneController, + enabled: !isLoading, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Phone number is required'; + } + return null; + }, + ), + ), + + SizedBox(height: 20.h), + + // Location Details + Align( + alignment: Alignment.centerLeft, + child: CustomText( + text: "Location Details", + size: 18.sp, + weight: FontWeight.w500, + ), + ), + SizedBox(height: 16.h), + + // Address 1 + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0.w), + child: CustomTextField( + label: "Address 1", + hint: "Enter address manually or tap to search", + controller: address1Controller, + enabled: !isLoading, + ), + ), + + // Address 2 + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0.w), + child: CustomTextField( + label: "Address 2 (Optional)", + hint: "Enter additional address details", + controller: address2Controller, + enabled: !isLoading, + ), + ), + + SizedBox(height: 26.h), + + // Buttons + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFF95F62), + side: const BorderSide( + color: Colors.transparent), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(38.r), + ), + padding: EdgeInsets.symmetric(vertical: 12.h), + ), + onPressed: isLoading + ? null + : () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + SizedBox(width: 16.w), + Expanded( + 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 : _saveProfile, + child: isLoading + ? SizedBox( + height: 20.h, + width: 20.w, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + "Save", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ], + ), + SizedBox(height: 20.h), + ], + ), + ), + ), + + // Loading overlay when saving + if (state is ProfileUpdating) + Container( + color: Colors.black.withOpacity(0.3), + child: Center( + child: Card( + child: Padding( + padding: EdgeInsets.all(20.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + color: Color(0xFFF95F62), + ), + SizedBox(height: 16.h), + Text( + 'Updating profile...', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, ); } -} +} \ No newline at end of file diff --git a/lib/home/views/home_page_view.dart b/lib/home/views/home_page_view.dart index 048a249..07659a1 100644 --- a/lib/home/views/home_page_view.dart +++ b/lib/home/views/home_page_view.dart @@ -1,3 +1,4 @@ +import 'package:citycards_customer/itinerary_creation/views/magic_itinerary_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:citycards_customer/common_packages/custom_bottom_navbar.dart'; @@ -6,6 +7,7 @@ import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_s import 'package:citycards_customer/my_pass/views/my_pass_page_view.dart'; import 'package:citycards_customer/postcard/views/postcard_initial_page_view.dart'; import '../../common_bloc/bottom_navigation_bloc.dart'; +import '../../itinerary_creation/views/magic_itinerary_empty_view.dart'; import 'registered_user_home_page.dart'; class HomePage extends StatefulWidget { @@ -38,7 +40,7 @@ class _HomePageState extends State { buildOffstageNavigator( 1, currentIndex, - const ItineraryCreationStartPage(), + const MagicItineraryView(), _navigatorKeys[1], ), buildOffstageNavigator( diff --git a/lib/itinerary_creation/views/magic_itinerary_empty_view.dart b/lib/itinerary_creation/views/magic_itinerary_empty_view.dart index 780a537..8ca41f8 100644 --- a/lib/itinerary_creation/views/magic_itinerary_empty_view.dart +++ b/lib/itinerary_creation/views/magic_itinerary_empty_view.dart @@ -5,6 +5,8 @@ import 'package:citycards_customer/core/route_constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'itinerary_creation_start_view.dart'; + class MagicItineraryEmptyView extends StatelessWidget { const MagicItineraryEmptyView({super.key}); @@ -36,7 +38,12 @@ class MagicItineraryEmptyView extends StatelessWidget { SizedBox(height: 27.h), CustomFilledButton(onTap: (){ - Navigator.pushNamed(context, RouteConstants.itineraryCreationStart); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ItineraryCreationStartPage(), + ), + ); }, label: "Create My Itinerary", showArrow: true,) ], diff --git a/lib/itinerary_creation/views/magic_itinerary_filled_view.dart b/lib/itinerary_creation/views/magic_itinerary_filled_view.dart deleted file mode 100644 index cd8ae45..0000000 --- a/lib/itinerary_creation/views/magic_itinerary_filled_view.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:citycards_customer/common_packages/app_bar.dart'; -import 'package:citycards_customer/common_packages/custom_filled_button.dart'; -import 'package:citycards_customer/common_packages/custom_text.dart'; -import 'package:citycards_customer/core/route_constants.dart'; -import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class MagicItineraryFilledView extends StatelessWidget { - const MagicItineraryFilledView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color(0xFFFFF5F5), - body: SafeArea( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), - child: SingleChildScrollView( - child: Column( - children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: false,), - - SizedBox(height: 24.h), - ItineraryFilledCard(), - - SizedBox(height: 32.h), - - CustomPaint( - painter: DottedBorderPainter(), - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 24.h), - decoration: BoxDecoration( - color: Color(0xFFF95F62).withOpacity(0.25), - borderRadius: BorderRadius.circular(12.sp), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CustomText( - text: "Plan your next adventure", - color: Color(0xFF656565), - size: 14.sp, - ), - SizedBox(height: 16.h), - CustomFilledButton( - onTap: () { - Navigator.pushNamed(context, RouteConstants.itineraryCreationStart); - }, - label: "Create My Itinerary", - showArrow: true, - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} - -class ItineraryFilledCard extends StatelessWidget { - const ItineraryFilledCard({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), - decoration: BoxDecoration( - border: Border.all(color: Colors.black.withOpacity(0.12)), - borderRadius: BorderRadius.circular(12.r), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomText( - text: "Melbourne Unlimited Card", - size: 16.sp, - weight: FontWeight.w500, - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 2.h), - decoration: BoxDecoration( - color: Color(0xFF439F6E), - borderRadius: BorderRadius.circular(100.r), - ), - child: CustomText( - text: "Active", - size: 11.sp, - color: Colors.white, - ), - ), - ], - ), - SizedBox(height: 4.h), - - CustomText( - text: "Melbourne", - size: 12.sp, - color: Colors.black.withOpacity(0.4), - ), - SizedBox(height: 12.h), - Row( - children: [ - Image.asset("assets/icons/calender_filled.png", width: 16.sp), - SizedBox(width: 4.w), - CustomText(text: "7 days", color: Color(0xFF8E8E8E), size: 12.sp), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Icon( - Icons.location_on_rounded, - color: Color(0xFF8E8E8E), - size: 16.sp, - ), - SizedBox(width: 4.w), - CustomText( - text: "6 attractions", - color: Color(0xFF8E8E8E), - size: 12.sp, - ), - ], - ), - SizedBox(height: 8.h), - - Row( - children: [ - Icon(Icons.watch_later, color: Color(0xFF8E8E8E), size: 16.sp), - SizedBox(width: 4.w), - CustomText( - text: "Created 1/15/2024", - color: Color(0xFF8E8E8E), - size: 12.sp, - ), - ], - ), - - SizedBox(height: 12.h), - - InkWell( - onTap: (){ - Navigator.of(context).pushReplacementNamed(RouteConstants.yourItinerary); - }, - child: Container( - height: 43.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: Color(0xFFF95F62)), - ), - child: Center( - child: CustomText( - text: "View Itinerary", - size: 16.sp, - color: Color(0xFFF95F62), - weight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/itinerary_creation/views/magic_itinerary_view.dart b/lib/itinerary_creation/views/magic_itinerary_view.dart new file mode 100644 index 0000000..95f492f --- /dev/null +++ b/lib/itinerary_creation/views/magic_itinerary_view.dart @@ -0,0 +1,273 @@ +import 'package:citycards_customer/common_packages/app_bar.dart'; +import 'package:citycards_customer/common_packages/custom_filled_button.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:citycards_customer/core/route_constants.dart'; +import 'package:citycards_customer/itinerary_creation/views/itinerary_creation_start_view.dart'; +import 'package:citycards_customer/postcard/widgets/dotted_border_container.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../localPreference/local_preference.dart'; +import '../../login/view/login_email_bottomsheet.dart'; + +class MagicItineraryView extends StatefulWidget { + const MagicItineraryView({super.key}); + + @override + State createState() => _MagicItineraryViewState(); +} + +class _MagicItineraryViewState extends State { + bool isLoggedIn = false; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _checkLoginStatus(); + } + + Future _checkLoginStatus() async { + final loginStatus = await LocalPreference.getLogin(); + setState(() { + isLoggedIn = loginStatus; + isLoading = false; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0xFFFFF5F5), + body: SafeArea( + child: isLoading + ? Center(child: CircularProgressIndicator()) + : Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + child: SingleChildScrollView( + child: Column( + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: false, + ), + SizedBox(height: 24.h), + + // Show different UI based on login status + if (isLoggedIn) ...[ + ItineraryFilledCard(), + SizedBox(height: 32.h), + CustomPaint( + painter: DottedBorderPainter(), + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 24.h), + decoration: BoxDecoration( + color: Color(0xFFF95F62).withOpacity(0.25), + borderRadius: BorderRadius.circular(12.sp), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomText( + text: "Plan your next adventure", + color: Color(0xFF656565), + size: 14.sp, + ), + SizedBox(height: 16.h), + CustomFilledButton( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ItineraryCreationStartPage(), + ), + ); + }, + label: "Create My Itinerary", + showArrow: true, + ), + ], + ), + ), + ), + ] else ...[ + EmptyItineraryView(), + ], + ], + ), + ), + ), + ), + ); + } +} + +class EmptyItineraryView extends StatelessWidget { + const EmptyItineraryView({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox(height: 40.h), + + // Illustration image - replace with your asset path + Image.asset( + "assets/images/not_login.png", // Replace with your actual asset path + height: 300.h, + fit: BoxFit.contain, + ), + + SizedBox(height: 32.h), + + CustomText( + text: "You have not Logged in Yet! ☹️", + size: 18.sp, + weight: FontWeight.w600, + textAlign: TextAlign.center, + ), + + SizedBox(height: 12.h), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: CustomText( + text: "Log in or purchase a pass to unlock the magic itinerary!", + size: 14.sp, + color: Color(0xFF656565), + textAlign: TextAlign.center, + ), + ), + + SizedBox(height: 32.h), + + CustomFilledButton( + onTap: () { + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => const LoginEmailBottomsheet(), + ); + }, + label: "Log In", + showArrow: true, + ), + ], + ); + } +} + +class ItineraryFilledCard extends StatelessWidget { + const ItineraryFilledCard({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), + decoration: BoxDecoration( + border: Border.all(color: Colors.black.withOpacity(0.12)), + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomText( + text: "Melbourne Unlimited Card", + size: 16.sp, + weight: FontWeight.w500, + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 2.h), + decoration: BoxDecoration( + color: Color(0xFF439F6E), + borderRadius: BorderRadius.circular(100.r), + ), + child: CustomText( + text: "Active", + size: 11.sp, + color: Colors.white, + ), + ), + ], + ), + SizedBox(height: 4.h), + CustomText( + text: "Melbourne", + size: 12.sp, + color: Colors.black.withOpacity(0.4), + ), + SizedBox(height: 12.h), + Row( + children: [ + Image.asset("assets/icons/calender_filled.png", width: 16.sp), + SizedBox(width: 4.w), + CustomText(text: "7 days", color: Color(0xFF8E8E8E), size: 12.sp), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Icon( + Icons.location_on_rounded, + color: Color(0xFF8E8E8E), + size: 16.sp, + ), + SizedBox(width: 4.w), + CustomText( + text: "6 attractions", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + ], + ), + SizedBox(height: 8.h), + Row( + children: [ + Icon(Icons.watch_later, color: Color(0xFF8E8E8E), size: 16.sp), + SizedBox(width: 4.w), + CustomText( + text: "Created 1/15/2024", + color: Color(0xFF8E8E8E), + size: 12.sp, + ), + ], + ), + SizedBox(height: 12.h), + InkWell( + onTap: () { + Navigator.of(context) + .pushReplacementNamed(RouteConstants.yourItinerary); + }, + child: Container( + height: 43.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Color(0xFFF95F62)), + ), + child: Center( + child: CustomText( + text: "View Itinerary", + size: 16.sp, + color: Color(0xFFF95F62), + weight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/localPreference/local_database.dart b/lib/localPreference/local_database.dart index c8bd26b..764a184 100644 --- a/lib/localPreference/local_database.dart +++ b/lib/localPreference/local_database.dart @@ -56,6 +56,22 @@ class LocalDatabase { refresh_token_max_age INTEGER NOT NULL ) '''); + + /// USER DETAILS TABLE + await db.execute(''' + CREATE TABLE user_details ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + full_name TEXT NOT NULL, + email_address TEXT NOT NULL, + role TEXT NOT NULL, + role_id INTEGER NOT NULL + ) +'''); + + }, ); } diff --git a/lib/localPreference/local_preference.dart b/lib/localPreference/local_preference.dart index 79e1abb..0414915 100644 --- a/lib/localPreference/local_preference.dart +++ b/lib/localPreference/local_preference.dart @@ -141,6 +141,18 @@ class LocalPreference { ); } + /// Update only access token (for refresh token flow) + static Future setAccessToken(String accessToken) async { + final db = await LocalDatabase().database; + + await db.update( + 'user_tokens', + {'access_token': accessToken}, + where: 'id = ?', + whereArgs: [1], + ); + } + /// Get access token static Future getAccessToken() async { final db = await LocalDatabase().database; @@ -184,4 +196,49 @@ class LocalPreference { ); } + /// Set user details + static Future setUserDetails({ + required int userId, + required String firstName, + required String lastName, + required String fullName, + required String emailAddress, + required String role, + required int roleId, + }) async { + final db = await LocalDatabase().database; + + await db.insert( + 'user_details', + { + 'id': 1, + 'user_id': userId, + 'first_name': firstName, + 'last_name': lastName, + 'full_name': fullName, + 'email_address': emailAddress, + 'role': role, + 'role_id': roleId, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + /// Get userId + static Future getUserId() async { + final db = await LocalDatabase().database; + + final result = await db.query( + 'user_details', + where: 'id = ?', + whereArgs: [1], + ); + + if (result.isNotEmpty) { + return result.first['user_id'] as int; + } + return null; + } + + } \ 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 6b473ac..07b3378 100644 --- a/lib/login/bloc/verify/verify_bloc.dart +++ b/lib/login/bloc/verify/verify_bloc.dart @@ -32,6 +32,15 @@ class VerifyOtpBloc extends Bloc { refreshToken: userModel.refreshToken, refreshTokenMaxAge: userModel.refreshTokenMaxAge, ); + await LocalPreference.setUserDetails( + userId: userModel.user.id, + firstName: userModel.user.firstName, + lastName: userModel.user.lastName, + fullName: userModel.user.fullName, + emailAddress: userModel.user.emailAddress, + role: userModel.user.role, + roleId: userModel.user.roleId, + ); emit(VerifyOtpSuccess(response: userModel)); } catch (e) { emit(VerifyOtpError(errorMessage: e.toString())); diff --git a/lib/main.dart b/lib/main.dart index 3464ac9..67bd215 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ 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 'profile/bloc/profile/profile_bloc.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -60,6 +61,7 @@ class MyApp extends StatelessWidget { loginRepository: LoginRepository(), ), ), + BlocProvider(create: (context) => ProfileBloc()), ], child: MaterialApp( onGenerateRoute: _appRouter.onGenerateRoute, diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 5acaa44..00deac3 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -11,6 +11,7 @@ class ApiUrls { static const attractionDetails = "$baseUrl/mobile/list"; static const home = "$baseUrl/mobile"; static const faqPrivacyTerms = "$baseUrl/mobile/user/cms-data"; + static const userProfile = "$baseUrl/mobile/user"; //Post Apis diff --git a/lib/networkApiServices/network_api_services.dart b/lib/networkApiServices/network_api_services.dart index 6b1eff2..78f626e 100644 --- a/lib/networkApiServices/network_api_services.dart +++ b/lib/networkApiServices/network_api_services.dart @@ -187,21 +187,12 @@ class NetworkApiService { final response = await _dio.post( ApiUrls.refreshToken, - data: { - "refreshToken": refreshToken, - }, + data: {"refreshToken": refreshToken}, options: Options( - headers: { - 'Authorization': null, - }, + headers: {'Authorization': null}, ), ); - - await LocalPreference.setTokens( - accessToken: response.data['accessToken'], - refreshToken: response.data['refreshToken'], - refreshTokenMaxAge: response.data['refreshTokenMaxAge'], - ); + await LocalPreference.setAccessToken(response.data['accessToken']); return true; } catch (_) { diff --git a/lib/profile/bloc/profile/profile_bloc.dart b/lib/profile/bloc/profile/profile_bloc.dart new file mode 100644 index 0000000..78515a9 --- /dev/null +++ b/lib/profile/bloc/profile/profile_bloc.dart @@ -0,0 +1,81 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/foundation.dart'; +import '../../repository/profile_repository.dart'; +import 'profile_event.dart'; +import 'profile_state.dart'; + +class ProfileBloc extends Bloc { + final ProfileRepository _profileRepository; + + ProfileBloc({ProfileRepository? profileRepository}) + : _profileRepository = profileRepository ?? ProfileRepository(), + super(const ProfileInitial()) { + on(_onFetchProfile); + on(_onUpdateProfile); + on(_onResetProfile); + } + + /// Handle fetching user profile + Future _onFetchProfile( + FetchProfileEvent event, + Emitter emit, + ) async { + try { + emit(const ProfileLoading()); + + final profile = await _profileRepository.fetchUserProfile(); + + emit(ProfileLoaded(profile: profile)); + + if (kDebugMode) { + print( + '✅ Profile fetched successfully: ${profile.firstName} ${profile.lastName}', + ); + } + } catch (e) { + final errorMessage = e.toString(); + emit(ProfileError(message: errorMessage)); + + if (kDebugMode) { + print('❌ Error fetching profile: $errorMessage'); + } + } + } + + /// Handle updating user profile + Future _onUpdateProfile( + UpdateProfileEvent event, + Emitter emit, + ) async { + try { + emit(const ProfileUpdating()); + + final updatedProfile = await _profileRepository.updateUserProfile( + data: event.toJson(), + ); + + emit(ProfileUpdated(profile: updatedProfile)); + + if (kDebugMode) { + print( + '✅ Profile updated successfully: ${updatedProfile.firstName} ${updatedProfile.lastName}', + ); + } + } catch (e) { + final errorMessage = e.toString(); + emit(ProfileError(message: errorMessage)); + + if (kDebugMode) { + print('❌ Error updating profile: $errorMessage'); + } + } + } + + /// Handle resetting profile state + void _onResetProfile( + ResetProfileEvent event, + Emitter emit, + ) { + emit(const ProfileInitial()); + } +} diff --git a/lib/profile/bloc/profile/profile_event.dart b/lib/profile/bloc/profile/profile_event.dart new file mode 100644 index 0000000..12dc566 --- /dev/null +++ b/lib/profile/bloc/profile/profile_event.dart @@ -0,0 +1,62 @@ +import 'package:equatable/equatable.dart'; + +abstract class ProfileEvent extends Equatable { + const ProfileEvent(); + + @override + List get props => []; +} + +/// Event to fetch user profile +class FetchProfileEvent extends ProfileEvent { + final int userId; + + const FetchProfileEvent({required this.userId}); + + @override + List get props => [userId]; +} + +/// Event to update user profile +class UpdateProfileEvent extends ProfileEvent { + final int userId; + final String firstName; + final String lastName; + final String mobileNumber; + final String? address1; + final String? address2; + + const UpdateProfileEvent({ + required this.userId, + required this.firstName, + required this.lastName, + required this.mobileNumber, + this.address1, + this.address2, + }); + + @override + List get props => [ + userId, + firstName, + lastName, + mobileNumber, + address1, + address2, + ]; + + Map toJson() { + return { + 'firstName': firstName, + 'lastName': lastName, + 'mobileNumber': mobileNumber, + if (address1 != null && address1!.isNotEmpty) 'address1': address1, + if (address2 != null && address2!.isNotEmpty) 'address2': address2, + }; + } +} + +/// Event to reset profile state +class ResetProfileEvent extends ProfileEvent { + const ResetProfileEvent(); +} \ No newline at end of file diff --git a/lib/profile/bloc/profile/profile_state.dart b/lib/profile/bloc/profile/profile_state.dart new file mode 100644 index 0000000..e67bfec --- /dev/null +++ b/lib/profile/bloc/profile/profile_state.dart @@ -0,0 +1,58 @@ +import 'package:equatable/equatable.dart'; +import '../../models/profile_model.dart'; + +abstract class ProfileState extends Equatable { + const ProfileState(); + + @override + List get props => []; +} + +/// Initial state +class ProfileInitial extends ProfileState { + const ProfileInitial(); +} + +/// Loading state for fetching profile +class ProfileLoading extends ProfileState { + const ProfileLoading(); +} + +/// Success state when profile is fetched +class ProfileLoaded extends ProfileState { + final ProfileModel profile; + + const ProfileLoaded({required this.profile}); + + @override + List get props => [profile]; +} + +/// Loading state for updating profile +class ProfileUpdating extends ProfileState { + const ProfileUpdating(); +} + +/// Success state when profile is updated +class ProfileUpdated extends ProfileState { + final ProfileModel profile; + final String message; + + const ProfileUpdated({ + required this.profile, + this.message = 'Profile updated successfully', + }); + + @override + List get props => [profile, message]; +} + +/// Error state +class ProfileError extends ProfileState { + final String message; + + const ProfileError({required this.message}); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/profile/models/profile_model.dart b/lib/profile/models/profile_model.dart new file mode 100644 index 0000000..fd70a05 --- /dev/null +++ b/lib/profile/models/profile_model.dart @@ -0,0 +1,171 @@ +class ProfileModel { + final int id; + final String firstName; + final String lastName; + final int roleXid; + final String emailAddress; + final String isdCode; + final String mobileNumber; + final String? address1; + final String? address2; + final String? cityName; + final String? zipCode; + final String? stateName; + final String? country; + final String? timezone; + final String? lastLogin; + final bool isActive; + final String createdAt; + final String updatedAt; + final RoleModel? role; + + ProfileModel({ + required this.id, + required this.firstName, + required this.lastName, + required this.roleXid, + required this.emailAddress, + required this.isdCode, + required this.mobileNumber, + this.address1, + this.address2, + this.cityName, + this.zipCode, + this.stateName, + this.country, + this.timezone, + this.lastLogin, + required this.isActive, + required this.createdAt, + required this.updatedAt, + this.role, + }); + + factory ProfileModel.fromJson(Map json) { + return ProfileModel( + id: json['id'] ?? 0, + firstName: json['firstName'] ?? 'N/A', + lastName: json['lastName'] ?? 'N/A', + roleXid: json['roleXid'] ?? 0, + emailAddress: json['emailAddress'] ?? 'N/A', + isdCode: json['isdCode'] ?? 'N/A', + mobileNumber: json['mobileNumber'] ?? 'N/A', + address1: json['address1'], + address2: json['address2'], + cityName: json['cityName'], + zipCode: json['zipCode'], + stateName: json['stateName'], + country: json['country'], + timezone: json['timezone'], + lastLogin: json['lastLogin'], + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', + role: json['role'] != null ? RoleModel.fromJson(json['role']) : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'roleXid': roleXid, + 'emailAddress': emailAddress, + 'isdCode': isdCode, + 'mobileNumber': mobileNumber, + 'address1': address1, + 'address2': address2, + 'cityName': cityName, + 'zipCode': zipCode, + 'stateName': stateName, + 'country': country, + 'timezone': timezone, + 'lastLogin': lastLogin, + 'isActive': isActive, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + if (role != null) 'role': role!.toJson(), + }; + } + + ProfileModel copyWith({ + int? id, + String? firstName, + String? lastName, + int? roleXid, + String? emailAddress, + String? isdCode, + String? mobileNumber, + String? address1, + String? address2, + String? cityName, + String? zipCode, + String? stateName, + String? country, + String? timezone, + String? lastLogin, + bool? isActive, + String? createdAt, + String? updatedAt, + RoleModel? role, + }) { + return ProfileModel( + id: id ?? this.id, + firstName: firstName ?? this.firstName, + lastName: lastName ?? this.lastName, + roleXid: roleXid ?? this.roleXid, + emailAddress: emailAddress ?? this.emailAddress, + isdCode: isdCode ?? this.isdCode, + mobileNumber: mobileNumber ?? this.mobileNumber, + address1: address1 ?? this.address1, + address2: address2 ?? this.address2, + cityName: cityName ?? this.cityName, + zipCode: zipCode ?? this.zipCode, + stateName: stateName ?? this.stateName, + country: country ?? this.country, + timezone: timezone ?? this.timezone, + lastLogin: lastLogin ?? this.lastLogin, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + role: role ?? this.role, + ); + } +} + +class RoleModel { + final int id; + final String name; + final bool isActive; + final String createdAt; + final String updatedAt; + + RoleModel({ + required this.id, + required this.name, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory RoleModel.fromJson(Map json) { + return RoleModel( + id: json['id'] ?? 0, + name: json['name'] ?? 'N/A', + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? 'N/A', + updatedAt: json['updatedAt'] ?? 'N/A', + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'isActive': isActive, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + }; + } +} \ No newline at end of file diff --git a/lib/profile/repository/profile_repository.dart b/lib/profile/repository/profile_repository.dart new file mode 100644 index 0000000..a0321a6 --- /dev/null +++ b/lib/profile/repository/profile_repository.dart @@ -0,0 +1,33 @@ +import '../models/profile_model.dart'; +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../../localPreference/local_preference.dart'; + +class ProfileRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Fetch user profile (userId from local storage) + Future fetchUserProfile() async { + final int? userId = await LocalPreference.getUserId(); + + final response = await _apiService.getApi( + url: '${ApiUrls.userProfile}/$userId', + ); + + return ProfileModel.fromJson(response.data); + } + + /// Update user profile (userId from local storage) + Future updateUserProfile({ + required Map data, + }) async { + final int? userId = await LocalPreference.getUserId(); + + final response = await _apiService.putApi( + url: '${ApiUrls.userProfile}/$userId', + data: data, + ); + + return ProfileModel.fromJson(response.data); + } +} diff --git a/lib/profile/view/profile_page_view.dart b/lib/profile/view/profile_page_view.dart index ed7ccdb..ed47635 100644 --- a/lib/profile/view/profile_page_view.dart +++ b/lib/profile/view/profile_page_view.dart @@ -7,12 +7,56 @@ import 'package:citycards_customer/core/route_constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../localPreference/local_preference.dart'; +import '../../login/view/login_email_bottomsheet.dart'; +import '../bloc/profile/profile_bloc.dart'; +import '../bloc/profile/profile_event.dart'; +import '../bloc/profile/profile_state.dart'; +import '../models/profile_model.dart'; -class ProfilePage extends StatelessWidget { +class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + bool isLogin = false; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _checkLoginStatus(); + } + + Future _checkLoginStatus() async { + final loginStatus = await LocalPreference.getLogin(); + final userId = await LocalPreference.getUserId(); + + setState(() { + isLogin = loginStatus; + isLoading = false; + }); + + // Fetch profile data if user is logged in + if (loginStatus && userId != null) { + context.read().add(FetchProfileEvent(userId: userId)); + } + } + @override Widget build(BuildContext context) { + if (isLoading) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + return Scaffold( backgroundColor: Colors.white, body: SafeArea( @@ -21,94 +65,89 @@ class ProfilePage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - CommonAppBar(isWhiteLogo: false, isProfilePage: true, showDivider: true,), - backWidget(context,"My Profile", Colors.black), + CommonAppBar( + isWhiteLogo: false, + isProfilePage: true, + showDivider: true, + ), + backWidget(context, "My Profile", Colors.black), SizedBox(height: 29.h), - // Profile Image and Name - Row( - children: [ - CircleAvatar( - radius: 38.r, - backgroundImage: AssetImage( - "assets/images/profile_img.png", - ), - ), - SizedBox(width: 16.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Laysha Adams', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - // SizedBox(height: 4,), - Row( + + // Show different UI based on login status + if (!isLogin) ...[ + // Guest User UI + _buildGuestUI(context), + ] else ...[ + // Logged In User UI with BLoC + BlocBuilder( + builder: (context, state) { + if (state is ProfileLoading) { + return Center( + child: CircularProgressIndicator(), + ); + } else if (state is ProfileLoaded) { + return _buildLoggedInUI(context, state.profile); + } else if (state is ProfileUpdated) { + return _buildLoggedInUI(context, state.profile); + } else if (state is ProfileError) { + return Column( children: [ Icon( - Icons.location_on_sharp, - color: Color(0xFF8E8E8E), - size: 14.sp, + Icons.error_outline, + color: Colors.red, + size: 48.sp, ), - SizedBox(width: 4.w), + SizedBox(height: 16.h), Text( - 'Louisiana, United States', + 'Failed to load profile', style: TextStyle( - fontSize: 12.sp, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + Text( + state.message, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, color: Color(0xFF8E8E8E), ), ), + SizedBox(height: 16.h), + ElevatedButton( + onPressed: () async { + final userId = await LocalPreference.getUserId(); + if (userId != null) { + context.read().add( + FetchProfileEvent(userId: userId), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFFF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(38.r), + ), + ), + child: Text( + 'Retry', + style: TextStyle(color: Colors.white), + ), + ), ], - ), - ], - ), - ], - ), + ); + } + // Default fallback + return _buildLoggedInUI(context, null); + }, + ), + ], SizedBox(height: 30.h), - // Account Settings Section - Align( - alignment: Alignment.centerLeft, - child: CustomText( - text: "Account Settings", - weight: FontWeight.w500, - size: 18.sp, - ), - ), - SizedBox(height: 10.h), - - _buildListTile( - icon: "assets/icons/user_profile.png", - title: 'Edit profile', - onTap: () { - Navigator.pushNamed(context, RouteConstants.editProfile); - }, - ), - _buildListTile( - icon: "assets/icons/change_language.png", - title: 'Change language', - onTap: () { - showModalBottomSheet( - context: context, - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), - ), - ), - builder: (context) => BlocProvider( - create: (_)=> LanguageBloc(), - child: LanguageSelectionBottomsheet()), - ); - }, - ), - SizedBox(height: 24.h), - - // Support & Legal Section + // Support & Legal Section (Always visible) Align( alignment: Alignment.centerLeft, child: CustomText( @@ -153,25 +192,36 @@ class ProfilePage extends StatelessWidget { SizedBox(height: 22.h), - // Logout Button - SizedBox( - width: double.infinity, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: Color(0xFFF95F62), - side: const BorderSide(color: Color(0xFFF95F62)), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(38.r), + // Logout Button (Only for logged in users) + if (isLogin) + SizedBox( + width: double.infinity, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Color(0xFFF95F62), + side: const BorderSide(color: Color(0xFFF95F62)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(38.r), + ), + padding: EdgeInsets.symmetric(vertical: 6.h), + ), + onPressed: () async { + // Handle logout + // await LocalPreference.clearPreference(); + context.read().add(ResetProfileEvent()); + setState(() { + isLogin = false; + }); + }, + child: Text( + 'Log out', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), ), - padding: EdgeInsets.symmetric(vertical: 6.h), - ), - onPressed: () {}, - child: Text( - 'Log out', - style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600), ), ), - ), ], ), ), @@ -179,6 +229,224 @@ class ProfilePage extends StatelessWidget { ); } + // Guest User UI (Not logged in) + Widget _buildGuestUI(BuildContext context) { + return Column( + children: [ + // Greeting Text + Text( + 'Hey, Stranger! 👋', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + SizedBox(height: 8.h), + Text( + 'We are thrilled to have you on our app.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.sp, + color: Color(0xFF8E8E8E), + ), + ), + Text( + 'Why not make it official?', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.sp, + color: Color(0xFF8E8E8E), + ), + ), + + SizedBox(height: 24.h), + + // Illustration Image + Image.asset( + "assets/images/guest_illustration.png", + height: 200.h, + fit: BoxFit.contain, + ), + + SizedBox(height: 32.h), + + // Sign In Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFFF95F62), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(38.r), + ), + padding: EdgeInsets.symmetric(vertical: 12.h), + ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (_) => const LoginEmailBottomsheet(), + ); + }, + child: Text( + 'Sign in', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ], + ); + } + + // Logged In User UI with dynamic data from API + Widget _buildLoggedInUI(BuildContext context, ProfileModel? profile) { + // Construct full name + String fullName = 'User'; + if (profile != null) { + fullName = '${profile.firstName} ${profile.lastName}'.trim(); + if (fullName.isEmpty) { + fullName = 'User'; + } + } + + // Construct location + String location = 'Not specified'; + if (profile != null) { + List locationParts = []; + + if (profile.address1 != null && profile.address1!.isNotEmpty) { + locationParts.add(profile.address1!); + } + if (profile.address2 != null && profile.address2!.isNotEmpty) { + locationParts.add(profile.address2!); + } + + if (locationParts.isNotEmpty) { + location = locationParts.join(', '); + } + } + + return Column( + children: [ + // Profile Image and Name + Row( + children: [ + CircleAvatar( + radius: 38.r, + backgroundImage: AssetImage( + "assets/images/profile_img.png", + ), + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fullName, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + Row( + children: [ + Icon( + Icons.location_on_sharp, + color: Color(0xFF8E8E8E), + size: 14.sp, + ), + SizedBox(width: 4.w), + Expanded( + child: Text( + location, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xFF8E8E8E), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + + SizedBox(height: 30.h), + + // Account Settings Section + Align( + alignment: Alignment.centerLeft, + child: CustomText( + text: "Account Settings", + weight: FontWeight.w500, + size: 18.sp, + ), + ), + SizedBox(height: 10.h), + + _buildListTile( + icon: "assets/icons/user_profile.png", + title: 'Edit profile', + onTap: () async { + final result = await Navigator.pushNamed( + context, + RouteConstants.editProfile, + ); + + // Refresh profile if edit was successful + if (result == true) { + final userId = await LocalPreference.getUserId(); + if (userId != null) { + context.read().add( + FetchProfileEvent(userId: userId), + ); + } + } + }, + ), + _buildListTile( + icon: "assets/icons/change_language.png", + title: 'Change language', + onTap: () { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (context) => BlocProvider( + create: (_) => LanguageBloc(), + child: LanguageSelectionBottomsheet(), + ), + ); + }, + ), + SizedBox(height: 24.h), + ], + ); + } + Widget _buildListTile({ required String icon, required String title, @@ -202,4 +470,4 @@ class ProfilePage extends StatelessWidget { ), ); } -} +} \ No newline at end of file