pull/244/head
aamir-csol 19 hours ago
parent 203c025183
commit 4c56137f10

@ -25,7 +25,7 @@ class CalenderUtilsNew {
Future<void> 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<NavigationService>().navigatorKey.currentContext!),
GetIt.instance<NavigationService>().navigatorKey.currentContext!,

@ -80,8 +80,13 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
String _countdownText = "";
Function(void Function())? _modalSetState; // Callback for modal state updates
// GlobalKey for BellAnimatedSwitch to control it programmatically
final GlobalKey<BellAnimatedSwitchState> _bellSwitchKey = GlobalKey<BellAnimatedSwitchState>();
@override
void initState() {
super.initState();
scheduleMicrotask(() async {
if (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) {
myAppointmentsViewModel.getDoctorsRatingCheck(
@ -102,12 +107,12 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
}
});
// 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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
),
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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
});
},
);
// 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<AppointmentDetailsPage> {
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<AppointmentDetailsPage> {
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

@ -33,10 +33,10 @@ class BellAnimatedSwitch extends StatefulWidget {
});
@override
State<BellAnimatedSwitch> createState() => _BellAnimatedSwitchState();
State<BellAnimatedSwitch> createState() => BellAnimatedSwitchState();
}
class _BellAnimatedSwitchState extends State<BellAnimatedSwitch> with SingleTickerProviderStateMixin {
class BellAnimatedSwitchState extends State<BellAnimatedSwitch> with SingleTickerProviderStateMixin {
late bool _isActive;
late AnimationController _animationController;
late Animation<Alignment> _circleAlignment;
@ -51,11 +51,7 @@ class _BellAnimatedSwitchState extends State<BellAnimatedSwitch> 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<BellAnimatedSwitch> 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<BellAnimatedSwitch> 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<BellAnimatedSwitch> with SingleTick
),
],
),
child: Center(
child: _getIcon(),
),
child: Center(child: _getIcon()),
),
),
],

@ -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<PatientInformationStep> {
}
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),
),
);
}

@ -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<bool> 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<bool> _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<NavigationService>().navigatorKey.currentContext!),
GetIt.instance<NavigationService>().navigatorKey.currentContext!,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.calendarPermissionAlert.tr(),
isShowActionButtons: true,
onCancelTap: () {
GetIt.instance<NavigationService>().pop();
},
onConfirmTap: () async {
GetIt.instance<NavigationService>().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<Permission, PermissionStatus> 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<NavigationService>().navigatorKey.currentContext!),
GetIt.instance<NavigationService>().navigatorKey.currentContext!,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.calendarPermissionAlert.tr(),
isShowActionButtons: true,
onCancelTap: () {
GetIt.instance<NavigationService>().pop();
},
onConfirmTap: () async {
GetIt.instance<NavigationService>().pop();
openAppSettings();
}),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
} else {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: GetIt.instance<NavigationService>().navigatorKey.currentContext!),
GetIt.instance<NavigationService>().navigatorKey.currentContext!,
child: Utils.getWarningWidget(
loadingText: LocaleKeys.calendarPermissionAlert.tr(),
isShowActionButtons: true,
onCancelTap: () {
GetIt.instance<NavigationService>().pop();
},
onConfirmTap: () async {
GetIt.instance<NavigationService>().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<void> _showPermissionDeniedDialog() async {
BuildContext? context = GetIt.instance<NavigationService>().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<NavigationService>().pop();
},
onConfirmTap: () async {
GetIt.instance<NavigationService>().pop();
await openAppSettings();
},
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
Future<void> _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(

Loading…
Cancel
Save