From 6725ab028d5ec69dd9d7b006f90508cec054b87b Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 12 Jan 2026 15:55:41 +0300 Subject: [PATCH] doctor search filter design update & switch user bug fix. --- .../authentication_view_model.dart | 11 +- .../book_appointments_view_model.dart | 37 ++++- .../my_appointments_view_model.dart | 1 + .../search_doctor_by_name.dart | 144 ++++++++++++++---- .../book_appointment/select_doctor_page.dart | 80 +++++----- .../book_appointment/widgets/doctor_card.dart | 2 +- 6 files changed, 203 insertions(+), 72 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index b393772..6379456 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -246,7 +246,6 @@ class AuthenticationViewModel extends ChangeNotifier { } Future _handleNewImeiRegistration() async { - await selectDeviceImei(onSuccess: (dynamic respData) async { try { if (respData != null) { @@ -461,6 +460,12 @@ class AuthenticationViewModel extends ChangeNotifier { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); MyAppointmentsViewModel myAppointmentsVM = getIt(); + if (isSwitchUser && _appState.getSuperUserID == null) { + nationalIdController.text = responseID.toString(); + }else if( isSwitchUser && _appState.getSuperUserID != null){ + nationalIdController.text = _appState.getSuperUserID.toString(); + } + final request = RequestUtils.getCommonRequestWelcome( phoneNumber: phoneNumberController.text, otpTypeEnum: otpTypeEnum, @@ -761,14 +766,14 @@ class AuthenticationViewModel extends ChangeNotifier { phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") : _appState.getAuthenticatedUser()!.mobileNumber)!; - nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; + nationalIdController.text = _appState.getAuthenticatedUser()!.patientIdentificationNo!; onSuccess(); } else if ((loginTypeEnum == LoginTypeEnum.sms || loginTypeEnum == LoginTypeEnum.whatsapp && _appState.getSelectDeviceByImeiRespModelElement == null) && _appState.getAuthenticatedUser() != null) { phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") : _appState.getAuthenticatedUser()!.mobileNumber)!; - nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; + nationalIdController.text = _appState.getAuthenticatedUser()!.patientIdentificationNo!; onSuccess(); } } diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index aa1467b..a225c1d 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -53,6 +53,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { int? calculationID = 0; bool isSortByClinic = true; + // Accordion expansion state + int? expandedGroupIndex; + int initialSlotDuration = 0; bool isNearestAppointmentSelected = false; @@ -158,13 +161,28 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool isBodyPartsLoading = false; int duration = 0; - setIsSortByClinic(bool value) { isSortByClinic = value; doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital; notifyListeners(); } + // Toggle accordion expansion + void toggleGroupExpansion(int index) { + if (expandedGroupIndex == index) { + expandedGroupIndex = null; + } else { + expandedGroupIndex = index; + } + notifyListeners(); + } + + // Reset accordion expansion + void resetGroupExpansion() { + expandedGroupIndex = null; + notifyListeners(); + } + // Sort filtered doctor list by clinic or hospital void sortFilteredDoctorList(bool sortByClinic) { isSortByClinic = sortByClinic; @@ -186,6 +204,22 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + // Group filtered doctors list for accordion display + List> getGroupedFilteredDoctorsList() { + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var doctor in filteredDoctorList) { + final clinicKey = (doctor.clinicName ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(doctor); + + final hospitalKey = (doctor.projectName ?? 'Unknown').trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(doctor); + } + + return isSortByClinic ? clinicMap.values.toList() : hospitalMap.values.toList(); + } + // Group doctors by clinic and hospital void _groupDoctorsList() { final clinicMap = >{}; @@ -237,6 +271,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { isDoctorProfileLoading = true; isLiveCareSchedule = false; currentlySelectedHospitalFromRegionFlow = null; + expandedGroupIndex = null; // Reset accordion state clinicsList.clear(); doctorsList.clear(); liveCareClinicsList.clear(); diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 9bd48ec..3934bf7 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -726,6 +726,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } Future getPatientAppointmentQueueDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + //TODO: Discuss With Haroon, Is the User Has no data it return No Element Bad State; isAppointmentQueueDetailsLoading = true; notifyListeners(); final result = await myAppointmentsRepo.getPatientAppointmentQueueDetails( diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 559e1e5..eb6d06f 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -34,6 +34,14 @@ class _SearchDoctorByNameState extends State { TextEditingController searchEditingController = TextEditingController(); FocusNode textFocusNode = FocusNode(); late BookAppointmentsViewModel bookAppointmentsViewModel; + late ScrollController _scrollController; + final Map _itemKeys = {}; + + @override + void initState() { + _scrollController = ScrollController(); + super.initState(); + } @override Widget build(BuildContext context) { @@ -173,8 +181,11 @@ class _SearchDoctorByNameState extends State { padding: EdgeInsets.only(top: 20.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.filteredDoctorList.length, + itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.getGroupedFilteredDoctorsList().length, itemBuilder: (context, index) { + final isExpanded = bookAppointmentsVM.expandedGroupIndex == index; + final groupedDoctors = bookAppointmentsVM.getGroupedFilteredDoctorsList(); + return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( doctorsListResponseModel: DoctorsListResponseModel(), @@ -188,35 +199,113 @@ class _SearchDoctorByNameState extends State { verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( + key: _itemKeys.putIfAbsent(index, () => GlobalKey()), duration: Duration(milliseconds: 300), curve: Curves.easeInOut, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: DoctorCard( - doctorsListResponseModel: bookAppointmentsVM.filteredDoctorList[index], - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); - // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), + child: InkWell( + onTap: () { + bookAppointmentsVM.toggleGroupExpansion(index); + // After rebuild, ensure the expanded item is visible + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _itemKeys[index]; + if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) { + Scrollable.ensureVisible( + key.currentContext!, + duration: Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.1, + ); + } + }); + }, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${groupedDoctors[index].length} ${'doctors'.needTranslation}", + onPressed: () {}, + backgroundColor: AppColors.greyColor, + borderColor: AppColors.greyColor, + textColor: AppColors.blackColor, + fontSize: 10, + fontWeight: FontWeight.w500, + borderRadius: 8, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 30.h), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + // Clinic/Hospital name as group title + Text( + bookAppointmentsVM.isSortByClinic + ? (groupedDoctors[index].first.clinicName ?? 'Unknown') + : (groupedDoctors[index].first.projectName ?? 'Unknown'), + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + // Expanded content - list of doctors in this group + AnimatedSwitcher( + duration: Duration(milliseconds: 400), + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.only(top: 12.h), + child: Column( + children: groupedDoctors[index].asMap().entries.map((entry) { + final doctorIndex = entry.key; + final doctor = entry.value; + final isLastDoctor = doctorIndex == groupedDoctors[index].length - 1; + + return Column( + children: [ + DoctorCard( + doctorsListResponseModel: doctor, + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(doctor); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile( + onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + }), + if (!isLastDoctor) + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + ], + ); + }).toList(), + ), + ) + : SizedBox.shrink(), + ), + ], + ), + ), + ), ), ), ), @@ -296,6 +385,7 @@ class _SearchDoctorByNameState extends State { @override void dispose() { + _scrollController.dispose(); bookAppointmentsViewModel.doctorsList.clear(); super.dispose(); } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 569e8ac..28b0f61 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -33,7 +33,6 @@ class SelectDoctorPage extends StatefulWidget { class _SelectDoctorPageState extends State { TextEditingController searchEditingController = TextEditingController(); - int? expandedIndex; FocusNode textFocusNode = FocusNode(); @@ -170,8 +169,7 @@ class _SelectDoctorPageState extends State { ), ], ).paddingSymmetrical(0.h, 0.h), - if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) - SizedBox(height: 16.h), + if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) SizedBox(height: 16.h), Row( mainAxisSize: MainAxisSize.max, children: [ @@ -190,7 +188,6 @@ class _SelectDoctorPageState extends State { value: bookAppointmentsVM.isNearestAppointmentSelected, onChanged: (newValue) async { bookAppointmentsVM.setIsNearestAppointmentSelected(newValue); - }, ), ], @@ -199,11 +196,9 @@ class _SelectDoctorPageState extends State { padding: EdgeInsets.only(top: 16.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isDoctorsListLoading - ? 5 - : (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1), + itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1), itemBuilder: (context, index) { - final isExpanded = expandedIndex == index; + final isExpanded = bookAppointmentsVM.expandedGroupIndex == index; return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( doctorsListResponseModel: DoctorsListResponseModel(), @@ -225,13 +220,11 @@ class _SelectDoctorPageState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), child: InkWell( onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); + bookAppointmentsVM.toggleGroupExpansion(index); // After rebuild, ensure the expanded item is visible WidgetsBinding.instance.addPostFrameCallback((_) { final key = _itemKeys[index]; - if (key != null && key.currentContext != null && expandedIndex == index) { + if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) { Scrollable.ensureVisible( key.currentContext!, duration: Duration(milliseconds: 350), @@ -282,34 +275,41 @@ class _SelectDoctorPageState extends State { key: ValueKey(index), padding: EdgeInsets.only(top: 12.h), child: Column( - children: bookAppointmentsVM.doctorsListGrouped[index].map((doctor) { - return Container( - margin: EdgeInsets.only(bottom: 12.h), - child: DoctorCard( - doctorsListResponseModel: doctor, - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(doctor); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), + children: bookAppointmentsVM.doctorsListGrouped[index].asMap().entries.map((entry) { + final doctorIndex = entry.key; + final doctor = entry.value; + final isLastDoctor = doctorIndex == bookAppointmentsVM.doctorsListGrouped[index].length - 1; + + return Column( + children: [ + DoctorCard( + doctorsListResponseModel: doctor, + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(doctor); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + if (!isLastDoctor) + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + ], ); }).toList(), ), diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index ffe26ff..0f30c5f 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -38,7 +38,7 @@ class DoctorCard extends StatelessWidget { hasShadow: false, ), child: Padding( - padding: EdgeInsets.all(14.h), + padding: EdgeInsets.only(top: 14.h,bottom: 20.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ -- 2.30.2