From 4c56137f102017f125a10dd15a906a186ad082d5 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 14 Apr 2026 11:49:14 +0300 Subject: [PATCH] calender --- lib/core/utils/calender_utils_new.dart | 2 +- .../appointment_details_page.dart | 122 ++++++++------- .../book_appointment/widgets/bell_switch.dart | 54 ++++--- .../widget/e_referral_patient_info.dart | 52 ++++--- lib/widgets/common_bottom_sheet.dart | 142 ++++++++++-------- 5 files changed, 204 insertions(+), 168 deletions(-) diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index dfb400be..347c2ebc 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -25,7 +25,7 @@ class CalenderUtilsNew { Future getCalenders() async { CalendarPermissionStatus result = await DeviceCalendar.instance.hasPermissions(); if (result != CalendarPermissionStatus.granted) { - // await DeviceCalendar.instance.requestPermissions(); + await DeviceCalendar.instance.requestPermissions(); showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: GetIt.instance().navigatorKey.currentContext!), GetIt.instance().navigatorKey.currentContext!, diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 5b012872..40ef4644 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -80,8 +80,13 @@ class _AppointmentDetailsPageState extends State { String _countdownText = ""; Function(void Function())? _modalSetState; // Callback for modal state updates + // GlobalKey for BellAnimatedSwitch to control it programmatically + final GlobalKey _bellSwitchKey = GlobalKey(); + @override void initState() { + super.initState(); + scheduleMicrotask(() async { if (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { myAppointmentsViewModel.getDoctorsRatingCheck( @@ -102,12 +107,12 @@ class _AppointmentDetailsPageState extends State { } }); - // Initialize countdown timer for payment - if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || widget.patientAppointmentHistoryResponseModel.nextAction == 20) { - _startCountdownTimer(); - } - - super.initState(); + // Initialize countdown timer for payment after first frame to ensure context is available + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _startCountdownTimer(); + } + }); } /// Checks if a reminder exists in the calendar for this appointment @@ -364,56 +369,51 @@ class _AppointmentDetailsPageState extends State { onRescheduleTap: () async { openDoctorScheduleCalendar(); }, - timerWidget: Column( - children: [ - SizedBox( - height: 16.w, - ), - Divider( - height: 1, - color: AppColors.dividerColor, - ), - SizedBox( - height: 16.w, - ), - Directionality( - textDirection: ui.TextDirection.ltr, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + timerWidget: (_timeRemaining == null || _timeRemaining == Duration.zero) + ? null + : Column( children: [ - Expanded( - child: _buildTimeUnit( - _timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00', - LocaleKeys.days.tr(context: context), - ), - ), - _buildTimeSeparator(), - Expanded( - child: _buildTimeUnit( - _timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00', - LocaleKeys.hours.tr(context: context), - ), - ), - _buildTimeSeparator(), - Expanded( - child: _buildTimeUnit( - _timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00', - LocaleKeys.minutes.tr(context: context), - ), - ), - _buildTimeSeparator(), - Expanded( - child: _buildTimeUnit( - _timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00', - LocaleKeys.seconds.tr(context: context), + SizedBox(height: 16.w), + Divider(height: 1, color: AppColors.dividerColor), + SizedBox(height: 16.w), + Directionality( + textDirection: ui.TextDirection.ltr, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildTimeUnit( + _timeRemaining!.inDays.toString().padLeft(2, '0'), + LocaleKeys.days.tr(context: context), + ), + ), + _buildTimeSeparator(), + Expanded( + child: _buildTimeUnit( + _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0'), + LocaleKeys.hours.tr(context: context), + ), + ), + _buildTimeSeparator(), + Expanded( + child: _buildTimeUnit( + _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0'), + LocaleKeys.minutes.tr(context: context), + ), + ), + _buildTimeSeparator(), + Expanded( + child: _buildTimeUnit( + _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0'), + LocaleKeys.seconds.tr(context: context), + ), + ), + ], ), ), ], ), - ), - ], - ), ), SizedBox(height: 16.h), @@ -603,6 +603,7 @@ class _AppointmentDetailsPageState extends State { ), const Spacer(), BellAnimatedSwitch( + key: _bellSwitchKey, initialValue: widget.patientAppointmentHistoryResponseModel.hasReminder ?? false, activeColor: AppColors.successColor.withOpacity(0.2), inactiveColor: AppColors.lightGrayBGColor, @@ -618,7 +619,9 @@ class _AppointmentDetailsPageState extends State { if (newValue == true) { DateTime startDate = DateTime.now(); DateTime endDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate); - BottomSheetUtils().showReminderBottomSheet( + + // Show reminder bottom sheet and check if permission was granted + bool permissionGranted = await BottomSheetUtils().showReminderBottomSheet( context, endDate, widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "", @@ -649,6 +652,11 @@ class _AppointmentDetailsPageState extends State { }); }, ); + + // If permission was not granted, revert the switch back to OFF + if (!permissionGranted) { + _bellSwitchKey.currentState?.setSwitchValue(false); + } } else { isEventAddedOrRemoved = await calender.checkAndRemove( id: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}", @@ -1210,8 +1218,7 @@ class _AppointmentDetailsPageState extends State { handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction); }, backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction), - borderColor: - AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, @@ -1392,9 +1399,10 @@ class _AppointmentDetailsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Lottie.asset(AppAnimations.warningAnimation, - repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), - SizedBox(height: 12,), + Lottie.asset(AppAnimations.warningAnimation, repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + SizedBox( + height: 12, + ), LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(color: AppColors.textColor, isCenter: true), SizedBox(height: 24.h), // // Countdown Timer - DD : HH : MM : SS format with labels - Always LTR diff --git a/lib/presentation/book_appointment/widgets/bell_switch.dart b/lib/presentation/book_appointment/widgets/bell_switch.dart index 498ea6aa..ea3746a0 100644 --- a/lib/presentation/book_appointment/widgets/bell_switch.dart +++ b/lib/presentation/book_appointment/widgets/bell_switch.dart @@ -33,10 +33,10 @@ class BellAnimatedSwitch extends StatefulWidget { }); @override - State createState() => _BellAnimatedSwitchState(); + State createState() => BellAnimatedSwitchState(); } -class _BellAnimatedSwitchState extends State with SingleTickerProviderStateMixin { +class BellAnimatedSwitchState extends State with SingleTickerProviderStateMixin { late bool _isActive; late AnimationController _animationController; late Animation _circleAlignment; @@ -51,11 +51,7 @@ class _BellAnimatedSwitchState extends State with SingleTick vsync: this, ); - // Animation for the circle position (left/right) - _circleAlignment = AlignmentTween( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ).animate( + _circleAlignment = AlignmentTween(begin: Alignment.centerLeft, end: Alignment.centerRight).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); @@ -78,6 +74,24 @@ class _BellAnimatedSwitchState extends State with SingleTick super.dispose(); } + /// Public method to set switch value programmatically without triggering onChanged callback + void setSwitchValue(bool value, {bool animate = true}) { + if (_isActive == value) return; // No change needed + + setState(() { + _isActive = value; + if (animate) { + if (_isActive) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + } else { + _animationController.value = _isActive ? 1.0 : 0.0; + } + }); + } + void _toggle() { setState(() { _isActive = !_isActive; @@ -106,17 +120,19 @@ class _BellAnimatedSwitchState extends State with SingleTick Widget _getIcon() { if (_isActive) { - return widget.activeIcon ?? Icon( - Icons.check, - size: 18, - color: widget.activeIconColor ?? Colors.green.shade400, - ); + return widget.activeIcon ?? + Icon( + Icons.check, + size: 18, + color: widget.activeIconColor ?? Colors.green.shade400, + ); } - return widget.inactiveIcon ?? Icon( - Icons.close, - size: 18, - color: widget.inactiveIconColor ?? Colors.grey.shade600, - ); + return widget.inactiveIcon ?? + Icon( + Icons.close, + size: 18, + color: widget.inactiveIconColor ?? Colors.grey.shade600, + ); } @override @@ -162,9 +178,7 @@ class _BellAnimatedSwitchState extends State with SingleTick ), ], ), - child: Center( - child: _getIcon(), - ), + child: Center(child: _getIcon()), ), ), ], diff --git a/lib/presentation/e_referral/widget/e_referral_patient_info.dart b/lib/presentation/e_referral/widget/e_referral_patient_info.dart index cbce6919..efbac168 100644 --- a/lib/presentation/e_referral/widget/e_referral_patient_info.dart +++ b/lib/presentation/e_referral/widget/e_referral_patient_info.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'dart:ui' as ui; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -138,30 +139,33 @@ class PatientInformationStepState extends State { } Widget _buildPatientPhoneField(ReferralFormManager formManager) { - return Focus( - focusNode: _phoneFocusNode, - child: TextInputWidget( - labelText: LocaleKeys.phoneNumber.tr(context: context), - hintText: "5xxxxxxxx", - controller: _phoneController, - padding: const EdgeInsets.all(8), - keyboardType: TextInputType.number, - fontFamily: "Poppins", - onChange: (value) { - formManager.updatePatientPhone(value ?? ''); - }, - onCountryChange: (value) { - formManager.updatePatientCountryEnum(value); - }, - prefix: '966', - isBorderAllowed: false, - isAllowLeadingIcon: true, - fontSize: 13, - isCountryDropDown: true, - leadingIcon: AppAssets.smart_phone, - errorMessage: formManager.errors.patientPhone, - hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientPhone) - ).paddingSymmetrical(0, 8), + return Directionality( + textDirection: ui.TextDirection.ltr, + child: Focus( + focusNode: _phoneFocusNode, + child: TextInputWidget( + labelText: LocaleKeys.phoneNumber.tr(context: context), + hintText: "5xxxxxxxx", + controller: _phoneController, + padding: const EdgeInsets.all(8), + keyboardType: TextInputType.number, + fontFamily: "Poppins", + onChange: (value) { + formManager.updatePatientPhone(value ?? ''); + }, + onCountryChange: (value) { + formManager.updatePatientCountryEnum(value); + }, + prefix: '966', + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 13, + isCountryDropDown: true, + leadingIcon: AppAssets.smart_phone, + errorMessage: formManager.errors.patientPhone, + hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientPhone) + ).paddingSymmetrical(0, 8), + ), ); } diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index 267fb5c5..069022d1 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -17,82 +17,92 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:permission_handler/permission_handler.dart'; class BottomSheetUtils { - showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, + Future showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, {required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false}) async { + // Check and request permissions based on platform + bool hasPermission = await _checkAndRequestCalendarPermissions(); + + if (hasPermission) { + // Permission granted, show the reminder selection bottom sheet + await _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + return true; + } else { + await _showPermissionDeniedDialog(); + return false; + } + } + + /// Checks and requests calendar permissions for both Android and iOS + /// Returns true if all required permissions are granted + Future _checkAndRequestCalendarPermissions() async { if (Platform.isAndroid) { + // Android: Check existing permission first using PermissionService bool hasPermission = await PermissionService.isCalendarPermissionEnabled(); - if (!hasPermission) { - PermissionStatus status = await Permission.calendarFullAccess.request(); - hasPermission = status.isGranted; - } if (hasPermission) { - _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, - onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); - } else { - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: GetIt.instance().navigatorKey.currentContext!), - GetIt.instance().navigatorKey.currentContext!, - child: Utils.getWarningWidget( - loadingText: LocaleKeys.calendarPermissionAlert.tr(), - isShowActionButtons: true, - onCancelTap: () { - GetIt.instance().pop(); - }, - onConfirmTap: () async { - GetIt.instance().pop(); - openAppSettings(); - }), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); + return true; + } + + // Request calendarFullAccess permission (Android 14+ requires this) + // This internally requests both READ_CALENDAR and WRITE_CALENDAR + PermissionStatus status = await Permission.calendarFullAccess.request(); + + // If calendarFullAccess request didn't show dialog or failed, + // fallback to requesting individual permissions + if (!status.isGranted) { + Map statuses = await [ + Permission.calendarWriteOnly, + ].request(); + + return statuses[Permission.calendarWriteOnly]?.isGranted ?? false; } + + return status.isGranted; } else { - if (await Permission.calendarWriteOnly.request().isGranted) { - if (await Permission.calendarFullAccess.request().isGranted) { - _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, - onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); - } else { - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: GetIt.instance().navigatorKey.currentContext!), - GetIt.instance().navigatorKey.currentContext!, - child: Utils.getWarningWidget( - loadingText: LocaleKeys.calendarPermissionAlert.tr(), - isShowActionButtons: true, - onCancelTap: () { - GetIt.instance().pop(); - }, - onConfirmTap: () async { - GetIt.instance().pop(); - openAppSettings(); - }), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } - } else { - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: GetIt.instance().navigatorKey.currentContext!), - GetIt.instance().navigatorKey.currentContext!, - child: Utils.getWarningWidget( - loadingText: LocaleKeys.calendarPermissionAlert.tr(), - isShowActionButtons: true, - onCancelTap: () { - GetIt.instance().pop(); - }, - onConfirmTap: () async { - GetIt.instance().pop(); - openAppSettings(); - }), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); + // iOS: Check if we already have full access + PermissionStatus fullAccessStatus = await Permission.calendarFullAccess.status; + + if (fullAccessStatus.isGranted) { + return true; + } + + // Request write permission first (required before full access on iOS) + PermissionStatus writeStatus = await Permission.calendarWriteOnly.request(); + if (!writeStatus.isGranted) { + return false; } + + // Then request full access permission + PermissionStatus fullStatus = await Permission.calendarFullAccess.request(); + return fullStatus.isGranted; } } + /// Shows permission denied dialog with option to open settings + Future _showPermissionDeniedDialog() async { + BuildContext? context = GetIt.instance().navigatorKey.currentContext; + if (context == null) return; + + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(context: context), + child: Utils.getWarningWidget( + loadingText: LocaleKeys.calendarPermissionAlert.tr(), + isShowActionButtons: true, + onCancelTap: () { + GetIt.instance().pop(); + }, + onConfirmTap: () async { + GetIt.instance().pop(); + await openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + Future _showReminderBottomSheet(BuildContext providedContext, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, {required Function onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool? isMultiAllowed}) async { showCommonBottomSheetWithoutHeight(providedContext, title: LocaleKeys.setTimerOfReminder.tr(context: providedContext), child: PrescriptionReminderView(