diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 5865a829..dc48f041 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -369,20 +369,23 @@ class BookAppointmentsViewModel extends ChangeNotifier { } void filterClinics(String? query) { - if (query!.isEmpty) { + if (query == null || query.trim().isEmpty) { _filteredClinicsList = List.from(clinicsList); showSortFilterButtons = false; } else { // Normalize the search query for better Arabic text matching - String normalizedQuery = Utils.normalizeArabicText(query.toLowerCase()); + String normalizedQuery = Utils.normalizeArabicText(query.trim().toLowerCase()); _filteredClinicsList = clinicsList.where((clinic) { String clinicName = clinic.clinicDescription?.toLowerCase() ?? ''; + String clinicNameN = clinic.clinicDescriptionN?.toLowerCase() ?? ''; String normalizedClinicName = Utils.normalizeArabicText(clinicName); - return normalizedClinicName.contains(normalizedQuery); + String normalizedClinicNameN = Utils.normalizeArabicText(clinicNameN); + return normalizedClinicName.contains(normalizedQuery) || + normalizedClinicNameN.contains(normalizedQuery); }).toList(); - showSortFilterButtons = query.length >= 3; + showSortFilterButtons = query.trim().length >= 3; } notifyListeners(); } diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index c421561d..9db7c0e4 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -115,44 +115,52 @@ class _SelectClinicPageState extends State { backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: bookAppointmentsViewModel.isLiveCareSchedule ? LocaleKeys.selectLiveCareClinic.tr(context: context) : LocaleKeys.selectClinic.tr(context: context), + stickyWidgetHeight: 56.h, + stickyWidget: Padding( + padding: EdgeInsets.fromLTRB(24.h, 0, 24.h, 14.h), + child: Consumer( + builder: (context, _, __) { + return TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: LocaleKeys.clinicName.tr(context: context), + controller: searchEditingController, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + focusNode: textFocusNode, + suffix: searchEditingController.text.isNotEmpty + ? GestureDetector( + onTap: () { + searchEditingController.clear(); + bookAppointmentsViewModel.filterClinics(""); + textFocusNode.unfocus(); + }, + child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), + ) + : null, + onChange: (value) { + bookAppointmentsViewModel.filterClinics(value!); + }, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ); + }, + ), + ), child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.h), child: Consumer(builder: (context, bookAppointmentsVM, child) { return Column( children: [ - SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: LocaleKeys.clinicName.tr(context: context), - controller: searchEditingController, - isEnable: true, - prefix: null, - autoFocus: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - focusNode: textFocusNode, - suffix: searchEditingController.text.isNotEmpty - ? GestureDetector( - onTap: () { - searchEditingController.clear(); - bookAppointmentsViewModel.filterClinics(""); - textFocusNode.unfocus(); - }, - child: Utils.buildSvgWithAssets(icon: AppAssets.close_bottom_sheet_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown), - ) - : null, - onChange: (value) { - bookAppointmentsViewModel.filterClinics(value!); - }, - padding: EdgeInsets.symmetric( - vertical: ResponsiveExtension(10).h, - horizontal: ResponsiveExtension(15).h, - ), - ), bookAppointmentsVM.isLiveCareSchedule ? ListView.separated( - padding: EdgeInsets.only(top: 24.h), + padding: EdgeInsets.only(top: 12.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: bookAppointmentsVM.isClinicsListLoading @@ -199,7 +207,7 @@ class _SelectClinicPageState extends State { separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ) : ListView.separated( - padding: EdgeInsets.only(top: 24.h), + padding: EdgeInsets.only(top: 12.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.filteredClinicsList.length, diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index bd69fb7e..27ce63ef 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -60,6 +60,7 @@ import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.da import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_details_page.dart'; import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -429,13 +430,6 @@ class _MedicalFilePageState extends State { }, children: _buildVitalSignPages( vitalSign: hmgServicesVM.vitalSignList.first, - onTap: () { - Navigator.of(context).push( - CustomPageRoute( - page: VitalSignPage(), - ), - ); - }, currentPageIndex: hmgServicesVM.vitalSignCurrentPage, ), ), @@ -1496,10 +1490,20 @@ class _MedicalFilePageState extends State { ); } + void _openVitalSignDetails(VitalSignDetailsArgs args, VitalSignResModel? targetVitalSign) { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignDetailsPage( + args: args, + selectedVitalSign: targetVitalSign, + ), + ), + ); + } + // Build pages with 2 cards each List _buildVitalSignPages({ required VitalSignResModel vitalSign, - required VoidCallback onTap, required int currentPageIndex, }) { return [ @@ -1515,8 +1519,15 @@ class _MedicalFilePageState extends State { value: _formatValue(vitalSign.bodyMassIndex), unit: '', status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, - // status: "Overweight", - onTap: onTap, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.bmi, + title: LocaleKeys.bmi.tr(context: context), + icon: AppAssets.bmiVital, + unit: '', + ), + vitalSign, + ), ), ), SizedBox(width: 12.w), @@ -1527,7 +1538,15 @@ class _MedicalFilePageState extends State { value: _formatValue(vitalSign.heightCm), unit: LocaleKeys.cm.tr(), status: null, - onTap: onTap, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.height, + title: LocaleKeys.height.tr(context: context), + icon: AppAssets.heightVital, + unit: 'cm', + ), + vitalSign, + ), ), ), ], @@ -1535,39 +1554,64 @@ class _MedicalFilePageState extends State { ), // Page 2: Weight + Blood Pressure Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Row( - children: [ - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.weightVital, - label: LocaleKeys.weight.tr(context: context), - value: _formatValue(vitalSign.weightKg), - unit: LocaleKeys.kg.tr(), - status: vitalSign.weightKg != null ? _getWeightStatus(vitalSign.bodyMassIndex) : null, - onTap: onTap, + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.weightVital, + label: LocaleKeys.weight.tr(context: context), + value: _formatValue(vitalSign.weightKg), + unit: LocaleKeys.kg.tr(), + status: vitalSign.weightKg != null ? _getWeightStatus(vitalSign.bodyMassIndex) : null, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.weight, + title: LocaleKeys.weight.tr(context: context), + icon: AppAssets.weightVital, + unit: 'kg', + ), + vitalSign, ), ), - SizedBox(width: 12.w), - Expanded( - child: _buildVitalSignCard( - icon: AppAssets.bloodPressure, - label: LocaleKeys.bloodPressure.tr(context: context), - value: (vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null && vitalSign.bloodPressureLower != 0 && vitalSign.bloodPressureHigher != 0) - ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" - : '--', - unit: '', - status: (vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null && vitalSign.bloodPressureLower != 0 && vitalSign.bloodPressureHigher != 0) - ? _getBloodPressureStatus( - systolic: vitalSign.bloodPressureHigher, - diastolic: vitalSign.bloodPressureLower, - ) - : null, - onTap: onTap, + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: LocaleKeys.bloodPressure.tr(context: context), + value: (vitalSign.bloodPressureLower != null && + vitalSign.bloodPressureHigher != null && + vitalSign.bloodPressureLower != 0 && + vitalSign.bloodPressureHigher != 0) + ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" + : '--', + unit: '', + status: (vitalSign.bloodPressureLower != null && + vitalSign.bloodPressureHigher != null && + vitalSign.bloodPressureLower != 0 && + vitalSign.bloodPressureHigher != 0) + ? _getBloodPressureStatus( + systolic: vitalSign.bloodPressureHigher, + diastolic: vitalSign.bloodPressureLower, + ) + : null, + onTap: () => _openVitalSignDetails( + VitalSignDetailsArgs( + metric: VitalSignMetric.bloodPressure, + title: LocaleKeys.bloodPressure.tr(context: context), + icon: AppAssets.bloodPressure, + unit: 'mmHg', + low: 90, + high: 140, + ), + vitalSign, ), ), - ], - )), + ), + ], + ), + ), ]; } diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index 0c677002..40ca8a98 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -13,28 +13,30 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import '../../core/dependencies.dart'; -class CollapsingListView extends StatelessWidget { +class CollapsingListView extends StatefulWidget { final String title; - Widget? child; - VoidCallback? search; - VoidCallback? report; - VoidCallback? logout; - VoidCallback? aiOverview; - VoidCallback? history; - VoidCallback? instructions; - VoidCallback? requests; - VoidCallback? sendEmail; - VoidCallback? doctorResponse; - VoidCallback? downloadReport; - VoidCallback? viewImage; - VoidCallback? location; - VoidCallback? downloadInvoice; - Widget? bottomChild; - Widget? trailing; - bool isClose; - bool isLeading; - VoidCallback? leadingCallback; - ScrollPhysics? physics; + final Widget? child; + final VoidCallback? search; + final VoidCallback? report; + final VoidCallback? logout; + final VoidCallback? aiOverview; + final VoidCallback? history; + final VoidCallback? instructions; + final VoidCallback? requests; + final VoidCallback? sendEmail; + final VoidCallback? doctorResponse; + final VoidCallback? downloadReport; + final VoidCallback? viewImage; + final VoidCallback? location; + final VoidCallback? downloadInvoice; + final Widget? bottomChild; + final Widget? trailing; + final bool isClose; + final bool isLeading; + final VoidCallback? leadingCallback; + final ScrollPhysics? physics; + final Widget? stickyWidget; + final double? stickyWidgetHeight; CollapsingListView({ super.key, @@ -59,9 +61,28 @@ class CollapsingListView extends StatelessWidget { this.trailing, this.leadingCallback, this.physics, + this.stickyWidget, + this.stickyWidgetHeight, }); - final ScrollController _controller = ScrollController(); + @override + State createState() => _CollapsingListViewState(); +} + +class _CollapsingListViewState extends State { + late final ScrollController _controller; + + @override + void initState() { + super.initState(); + _controller = ScrollController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -71,82 +92,66 @@ class CollapsingListView extends StatelessWidget { body: Column( children: [ CustomScrollView( - physics: physics, + physics: widget.physics, controller: _controller, slivers: [ SliverAppBar( - automaticallyImplyLeading: isLeading, - pinned: true, - toolbarHeight: 40.h, - leadingWidth: isLeading ? 40.w + 32.w : double.infinity, - systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), - surfaceTintColor: Colors.transparent, - backgroundColor: AppColors.bgScaffoldColor, - bottom: - // isLeading - // ? - ScrollAnimatedTitle( - title: title, - showBack: true, - controller: _controller, - search: search, - report: report, - logout: logout, - history: history, - instructions: instructions, - requests: requests, - sendEmail: sendEmail, - doctorResponse: doctorResponse, - downloadReport: downloadReport, - viewImage: viewImage, - location: location, - downloadInvoice: downloadInvoice, - bottomChild: bottomChild, - trailing: trailing, - aiOverview: aiOverview, - ), - // : null, - leading: Visibility( - visible: isLeading, - child: Container( - height: 32.h, - width: 32.w, - alignment: appState.isArabic() ? Alignment.centerRight : Alignment.centerLeft, - margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), - child: Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), - ), - ).onPress(() { - if (leadingCallback != null) { - leadingCallback!(); - } else { - context.pop(); - } - }), - ) - // : ScrollAnimatedTitle( - // title: title, - // showBack: false, - // controller: _controller, - // search: search, - // report: report, - // logout: logout, - // history: history, - // instructions: instructions, - // requests: requests, - // sendEmail: sendEmail, - // bottomChild: bottomChild, - // trailing: trailing, - // ), - ), - if (child != null) + automaticallyImplyLeading: widget.isLeading, + pinned: true, + toolbarHeight: 40.h, + leadingWidth: widget.isLeading ? 40.w + 32.w : double.infinity, + systemOverlayStyle: SystemUiOverlayStyle(statusBarBrightness: Brightness.light), + surfaceTintColor: Colors.transparent, + backgroundColor: AppColors.bgScaffoldColor, + bottom: ScrollAnimatedTitle( + title: widget.title, + showBack: true, + controller: _controller, + search: widget.search, + report: widget.report, + logout: widget.logout, + history: widget.history, + instructions: widget.instructions, + requests: widget.requests, + sendEmail: widget.sendEmail, + doctorResponse: widget.doctorResponse, + downloadReport: widget.downloadReport, + viewImage: widget.viewImage, + location: widget.location, + downloadInvoice: widget.downloadInvoice, + bottomChild: widget.bottomChild, + trailing: widget.trailing, + aiOverview: widget.aiOverview, + stickyWidget: widget.stickyWidget, + stickyWidgetHeight: widget.stickyWidgetHeight, + ), + leading: Visibility( + visible: widget.isLeading, + child: Container( + height: 32.h, + width: 32.w, + alignment: appState.isArabic() ? Alignment.centerRight : Alignment.centerLeft, + margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), + child: Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: widget.isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), + ), + ).onPress(() { + if (widget.leadingCallback != null) { + widget.leadingCallback!(); + } else { + context.pop(); + } + }), + ), + ), + if (widget.child != null) SliverToBoxAdapter( - child: child!, + child: widget.child!, ), ], ).expanded, - if (bottomChild != null) bottomChild! + if (widget.bottomChild != null) widget.bottomChild! ], ), ); @@ -216,6 +221,8 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget VoidCallback? downloadInvoice; Widget? bottomChild; Widget? trailing; + Widget? stickyWidget; + double? stickyWidgetHeight; ScrollAnimatedTitle({ super.key, @@ -237,10 +244,12 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget this.downloadInvoice, this.bottomChild, this.trailing, + this.stickyWidget, + this.stickyWidgetHeight, }); @override - Size get preferredSize => const Size.fromHeight(56.0); + Size get preferredSize => Size.fromHeight(56.0 + (stickyWidget != null ? (stickyWidgetHeight ?? 58.h) : 0)); @override State createState() => _ScrollAnimatedTitleState(); @@ -254,6 +263,7 @@ class _ScrollAnimatedTitleState extends State { static const double _minHeight = 56; double _fontSize = _maxFont; + double t = 1.0; @override void initState() { @@ -261,33 +271,53 @@ class _ScrollAnimatedTitleState extends State { widget.controller.addListener(_onScroll); } + @override + void didUpdateWidget(covariant ScrollAnimatedTitle oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_onScroll); + widget.controller.addListener(_onScroll); + _onScroll(); + } + } + @override void dispose() { widget.controller.removeListener(_onScroll); super.dispose(); } - double t = 1.0; - void _onScroll() { + if (!widget.controller.hasClients) return; final double offset = widget.controller.offset; // control animation range const double range = 120; - final double t = (1 - (offset / range)).clamp(0.0, 1.0); - this.t = t; - setState(() { - _fontSize = _minFont + (_maxFont - _minFont) * t; - }); + final double newT = (1 - (offset / range)).clamp(0.0, 1.0); + if (newT != t) { + setState(() { + t = newT; + _fontSize = _minFont + (_maxFont - _minFont) * t; + }); + } } @override Widget build(BuildContext context) { final isRtl = Directionality.of(context) == TextDirection.RTL; - return Container( + + final double titleHeight = widget.stickyWidget != null + ? (30.h + (44.h - 30.h) * t) + : 56.h; + + final double topGap = widget.stickyWidget != null + ? (2.h + (10.h - 2.h) * t) + : 0.0; + + final titleWidget = Container( // height: (widget.preferredSize.height - _fontSize / 2).h, - height: 56.h, + height: titleHeight, alignment: isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), padding: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), child: Row( @@ -322,6 +352,24 @@ class _ScrollAnimatedTitleState extends State { ], ), ); + + if (widget.stickyWidget == null) { + return titleWidget; + } + + return Container( + color: AppColors.bgScaffoldColor, + alignment: Alignment.topCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + titleWidget, + SizedBox(height: topGap), + widget.stickyWidget!, + ], + ), + ); } Widget actionButton(BuildContext context, double t, {required String title, required String icon, bool isAiButton = false}) {