dev_sultan #281

Merged
khansultan1 merged 2 commits from dev_sultan into master 17 hours ago

@ -1810,5 +1810,13 @@
"watchUsOnYoutube": "شاهدنا على إنستغرام",
"connectOnLinkedin": "تواصل معنا على لينكد إن",
"riskFactorPageTitle": "اعراض تشعر بها",
"symptomsCheckSuggestions": "الاحتمالات"
"symptomsCheckSuggestions": "الاحتمالات",
"bloodDonationService": "التبرع \n بالدم",
"bookAppointmentService": "حجز\nموعد",
"pinchToZoom": "اضغط للتكبير",
"gotIt": "فهمت",
"zoomIntoDetails": "قم بالتكبير لرؤية التفاصيل",
"pinchWithTwoFingersToZoom": "اضغط بإصبعين للتكبير",
"dragToMoveAround": "اسحب للتحرك",
"startExploring": "ابدأ الاستكشاف"
}

@ -1800,7 +1800,15 @@
"watchUsOnYoutube": "Watch us on Instagram",
"connectOnLinkedin": "Connect on LinkedIn",
"riskFactorPageTitle": "What do you feel?",
"symptomsCheckSuggestions": "Suggestions"
"symptomsCheckSuggestions": "Suggestions",
"bloodDonationService": "Blood Donation",
"bookAppointmentService": "Book\nAppointment",
"pinchToZoom": "Pinch to zoom",
"gotIt": "Got it",
"zoomIntoDetails": "Zoom into details",
"pinchWithTwoFingersToZoom": "Pinch with two fingers to zoom",
"dragToMoveAround": "Drag to move around",
"startExploring": "Start exploring"
}

@ -80,4 +80,5 @@ class CacheConst {
static const String zoomRoomID = 'zoom-room-id';
static const String callTypeID = 'call-type-id';
static String isAppOpenedFromCall = "is_app_opened_from_call";
static const String organSelectorTutorialShown = 'organ_selector_tutorial_shown';
}

@ -131,9 +131,13 @@ class _NewReferralPageState extends State<NewReferralPage> {
hmgServicesVM.createEReferral(
requestModel: createReferralRequestModel,
onSuccess: (GenericApiModel response) {
onSuccess: (GenericApiModel response) async{
LoaderBottomSheet.hideLoader();
showSuccessBottomSheet(int.parse(response.data), hmgServicesVM);
await Future.delayed(Duration(milliseconds: 2000)).then((value) {
if (mounted) Navigator.pop(context);
});
},
onError: (errorMessage) {
// Handle error (e.g., show error message)

@ -88,7 +88,7 @@ class _ServicesPageState extends State<ServicesPage> {
// await getIt.get<AuthenticationViewModel>().onLoginPressed();
// }
}),
HmgServicesComponentModel(11, LocaleKeys.bookAppointment.tr(), "", AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, route: null, onTap: () {
HmgServicesComponentModel(11, LocaleKeys.bookAppointmentService.tr(), "", AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, route: null, onTap: () {
getIt.get<BookAppointmentsViewModel>().onTabChanged(0);
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(CustomPageRoute(page: BookAppointmentPage()));
}),
@ -169,7 +169,7 @@ class _ServicesPageState extends State<ServicesPage> {
}),
HmgServicesComponentModel(
3,
LocaleKeys.bloodDonation.tr(context: getIt.get<NavigationService>().navigatorKey.currentContext!),
LocaleKeys.bloodDonationService.tr(context: getIt.get<NavigationService>().navigatorKey.currentContext!),
"".needTranslation,
AppAssets.blood_donation_icon,
bgColor: AppColors.bloodDonationCardColor,

@ -57,6 +57,7 @@ class ServiceGridViewItem extends StatelessWidget {
isBold: true,
color: AppColors.textColor,
maxLine: 2,
),
],
));

@ -178,13 +178,15 @@ class _MedicalFileAppointmentCardState extends State<MedicalFileAppointmentCard>
],
),
SizedBox(height: 12.h),
Row(
children: [
widget.myAppointmentsViewModel.isMyAppointmentsLoading
? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r)
: Expanded(
flex: 7,
child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
// Check if doctor is active - if not, show only View Details button
(widget.patientAppointmentHistoryResponseModel.isActiveDoctor ?? true)
? // Doctor is active - check rebooking logic
(AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) &&
widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false)
? // Show only the button without arrow when rebooking not allowed
widget.myAppointmentsViewModel.isMyAppointmentsLoading
? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r)
: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
? getArrivedAppointmentButton(context).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading)
: CustomButton(
text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction),
@ -202,51 +204,101 @@ class _MedicalFileAppointmentCardState extends State<MedicalFileAppointmentCard>
icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction),
iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction),
iconSize: 14.h,
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
),
SizedBox(width: 8.w),
((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel))
? SizedBox.shrink()
: Expanded(
flex: 2,
child: Container(
height: 40.h,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 10.r,
),
child: Padding(
padding: EdgeInsets.all(10.w),
child: Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
iconColor: AppColors.whiteColor,
icon: AppAssets.forward_arrow_icon_small,
width: 40.w,
height: 40.h,
fit: BoxFit.contain,
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading)
: // Normal flow - show button with arrow
Row(
children: [
widget.myAppointmentsViewModel.isMyAppointmentsLoading
? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r)
: Expanded(
flex: 7,
child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)
? getArrivedAppointmentButton(context).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading)
: CustomButton(
text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction),
onPressed: () {
handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context);
},
backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15),
borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction),
fontSize: 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction),
iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction),
iconSize: 14.h,
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
),
SizedBox(width: 8.w),
((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&
AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel))
? SizedBox.shrink()
: Expanded(
flex: 2,
child: Container(
height: 40.h,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 10.r,
),
child: Padding(
padding: EdgeInsets.all(10.w),
child: Transform.flip(
flipX: appState.isArabic(),
child: Utils.buildSvgWithAssets(
iconColor: AppColors.whiteColor,
icon: AppAssets.forward_arrow_icon_small,
width: 40.w,
height: 40.h,
fit: BoxFit.contain,
),
),
),
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((val) {
// widget.myAppointmentsViewModel.initAppointmentsViewModel();
// widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
}),
),
],
)
: // Doctor is not active - show only View Details button
CustomButton(
text: LocaleKeys.viewDetails.tr(context: context),
onPressed: () {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
),
),
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((val) {
// widget.myAppointmentsViewModel.initAppointmentsViewModel();
// widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
}),
),
],
),
)
.then((_) {
widget.myAppointmentsViewModel.initAppointmentsViewModel();
widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: (isFoldable || isTablet) ? 12.f : 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading),
],
).paddingAll(16.w),
),
@ -255,6 +307,34 @@ class _MedicalFileAppointmentCardState extends State<MedicalFileAppointmentCard>
}
Widget getArrivedAppointmentButton(BuildContext context) {
// Check if rebooking is not allowed - show View Details button
if (widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) {
return CustomButton(
text: LocaleKeys.viewDetails.tr(context: context),
onPressed: () {
Navigator.of(context)
.push(
CustomPageRoute(
page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel),
),
)
.then((_) {
widget.myAppointmentsViewModel.initAppointmentsViewModel();
widget.myAppointmentsViewModel.getPatientAppointments(true, false);
});
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: (isFoldable || isTablet) ? 12.f : 14.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
);
}
// Check if clinic allows rebooking or if doctor is excluded
if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
(widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) &&

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
@ -11,6 +12,8 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -28,12 +31,36 @@ class OrganSelectorPage extends StatefulWidget {
class _OrganSelectorPageState extends State<OrganSelectorPage> {
late final AppState _appState;
late final DialogService dialogService;
late final CacheService cacheService;
bool _showTutorial = false;
@override
void initState() {
super.initState();
_appState = getIt.get<AppState>();
dialogService = getIt<DialogService>();
cacheService = getIt<CacheService>();
_checkAndShowTutorial();
}
Future<void> _checkAndShowTutorial() async {
final hasSeenTutorial = cacheService.getBool(key: CacheConst.organSelectorTutorialShown) ?? false;
if (!hasSeenTutorial) {
// Show tutorial after a short delay to ensure the screen is fully built
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
setState(() {
_showTutorial = true;
});
}
}
}
void _onTutorialComplete() async {
await cacheService.saveBool(key: CacheConst.organSelectorTutorialShown, value: true);
setState(() {
_showTutorial = false;
});
}
void _onNextPressed(SymptomsCheckerViewModel viewModel) async {
@ -65,30 +92,39 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
appBar: _buildAppBar(),
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
SizedBox(height: 8.h),
Expanded(
child: Stack(
children: [
_buildBodyViewer(viewModel),
_buildViewToggleButtons(viewModel),
_buildViewZoomButtons(viewModel),
_buildBottomSheet(viewModel),
],
),
),
],
);
},
),
return Stack(
children: [
Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
appBar: _buildAppBar(),
body: Consumer<SymptomsCheckerViewModel>(
builder: (context, viewModel, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(),
SizedBox(height: 8.h),
Expanded(
child: Stack(
children: [
_buildBodyViewer(viewModel),
_buildViewToggleButtons(viewModel),
// _buildViewZoomButtons(viewModel),
_buildBottomSheet(viewModel),
],
),
),
],
);
},
),
),
// Tutorial overlay
if (_showTutorial)
PinchZoomTutorialOverlay(
onComplete: _onTutorialComplete,
),
],
);
}

@ -0,0 +1,261 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
class PinchZoomTutorialOverlay extends StatefulWidget {
final VoidCallback onComplete;
const PinchZoomTutorialOverlay({
Key? key,
required this.onComplete,
}) : super(key: key);
@override
State<PinchZoomTutorialOverlay> createState() => _PinchZoomTutorialOverlayState();
}
class _PinchZoomTutorialOverlayState extends State<PinchZoomTutorialOverlay> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
int _currentStep = 0;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _nextStep() {
if (_currentStep < 1) {
setState(() {
_currentStep++;
});
} else {
_animationController.reverse().then((_) {
widget.onComplete();
});
}
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Material(
color: Colors.black.withValues(alpha: 0.85),
child: SafeArea(
child: Stack(
children: [
// Tap to dismiss overlay
Positioned.fill(
child: GestureDetector(
onTap: () {},
child: Container(color: Colors.transparent),
),
),
// Tutorial content
Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: _currentStep == 0 ? _buildFirstStep() : _buildSecondStep(),
),
),
],
),
),
),
);
}
Widget _buildFirstStep() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Pinch gesture illustration
Container(
width: 200.w,
height: 200.h,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20.r),
),
child: Center(
child: _buildPinchGestureAnimation(),
),
),
SizedBox(height: 32.h),
// Title
Text(
LocaleKeys.pinchToZoom.tr(context: context),
style: TextStyle(
color: Colors.white,
fontSize: 24.f,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
SizedBox(height: 32.h),
// Button
CustomButton(
text: LocaleKeys.gotIt.tr(context: context),
onPressed: _nextStep,
backgroundColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
height: 50.h,
),
],
);
}
Widget _buildSecondStep() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Illustration container with rounded corners
Container(
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.95),
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
LocaleKeys.zoomIntoDetails.tr(context: context),
style: TextStyle(
color: AppColors.textColor,
fontSize: 20.f,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
SizedBox(height: 24.h),
// Pinch instruction
_buildInstructionRow(
icon: Icons.pinch_outlined,
text: LocaleKeys.pinchWithTwoFingersToZoom.tr(context: context),
),
SizedBox(height: 16.h),
// Drag instruction
_buildInstructionRow(
icon: Icons.pan_tool_outlined,
text: LocaleKeys.dragToMoveAround.tr(context: context),
),
],
),
),
SizedBox(height: 32.h),
// Button
CustomButton(
text: LocaleKeys.startExploring.tr(context: context),
onPressed: _nextStep,
backgroundColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
height: 50.h,
),
],
);
}
Widget _buildInstructionRow({required IconData icon, required String text}) {
return Row(
children: [
Container(
width: 48.w,
height: 48.h,
decoration: BoxDecoration(
color: AppColors.primaryRedColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
icon,
color: AppColors.primaryRedColor,
size: 24.w,
),
),
SizedBox(width: 16.w),
Expanded(
child: Text(
text,
style: TextStyle(
color: AppColors.textColor,
fontSize: 14.f,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
Widget _buildPinchGestureAnimation() {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 1.0, end: 0.7),
duration: const Duration(milliseconds: 1500),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Stack(
alignment: Alignment.center,
children: [
// Left hand
Positioned(
left: 40.w * (1 - value),
child: Transform.rotate(
angle: -0.3,
child: Icon(
Icons.pan_tool,
size: 50.w,
color: Colors.white.withValues(alpha: 0.9),
),
),
),
// Right hand
Positioned(
right: 40.w * (1 - value),
child: Transform.rotate(
angle: 0.3,
child: Icon(
Icons.pan_tool,
size: 50.w,
color: Colors.white.withValues(alpha: 0.9),
),
),
),
],
),
);
},
onEnd: () {
// Restart animation
setState(() {});
},
);
}
}
Loading…
Cancel
Save