pull/245/head
Sultan khan 2 days ago
commit 0a24e6cfd1

@ -25,7 +25,7 @@ class CalenderUtilsNew {
Future<void> getCalenders() async { Future<void> getCalenders() async {
CalendarPermissionStatus result = await DeviceCalendar.instance.hasPermissions(); CalendarPermissionStatus result = await DeviceCalendar.instance.hasPermissions();
if (result != CalendarPermissionStatus.granted) { if (result != CalendarPermissionStatus.granted) {
// await DeviceCalendar.instance.requestPermissions(); await DeviceCalendar.instance.requestPermissions();
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(
title: LocaleKeys.notice.tr(context: GetIt.instance<NavigationService>().navigatorKey.currentContext!), title: LocaleKeys.notice.tr(context: GetIt.instance<NavigationService>().navigatorKey.currentContext!),
GetIt.instance<NavigationService>().navigatorKey.currentContext!, GetIt.instance<NavigationService>().navigatorKey.currentContext!,

@ -2,6 +2,8 @@ import 'dart:developer';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:hmg_patient_app_new/core/app_state.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/enums.dart';
import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart'; import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart';
import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/blood_pressure_result.dart'; import 'package:hmg_patient_app_new/features/health_trackers/models/blood_pressure/blood_pressure_result.dart';
@ -76,6 +78,10 @@ class HealthTrackersViewModel extends ChangeNotifier {
"آخر", "آخر",
]; ];
List<String> getBloodSugarTimeList(){
return getIt.get<AppState>().isArabic()?bloodSugarMeasureTimeArList:bloodSugarMeasureTimeEnList;
}
// Setters with notification // Setters with notification
void setBloodSugarMeasureTime(String duration) async { void setBloodSugarMeasureTime(String duration) async {
_selectedBloodSugarMeasureTime = duration; _selectedBloodSugarMeasureTime = duration;

@ -80,8 +80,13 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
String _countdownText = ""; String _countdownText = "";
Function(void Function())? _modalSetState; // Callback for modal state updates Function(void Function())? _modalSetState; // Callback for modal state updates
// GlobalKey for BellAnimatedSwitch to control it programmatically
final GlobalKey<BellAnimatedSwitchState> _bellSwitchKey = GlobalKey<BellAnimatedSwitchState>();
@override @override
void initState() { void initState() {
super.initState();
scheduleMicrotask(() async { scheduleMicrotask(() async {
if (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { if (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) {
myAppointmentsViewModel.getDoctorsRatingCheck( myAppointmentsViewModel.getDoctorsRatingCheck(
@ -102,12 +107,12 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
} }
}); });
// Initialize countdown timer for payment // Initialize countdown timer for payment after first frame to ensure context is available
if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || widget.patientAppointmentHistoryResponseModel.nextAction == 20) { WidgetsBinding.instance.addPostFrameCallback((_) {
_startCountdownTimer(); if (mounted) {
} _startCountdownTimer();
}
super.initState(); });
} }
/// Checks if a reminder exists in the calendar for this appointment /// Checks if a reminder exists in the calendar for this appointment
@ -364,56 +369,51 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
onRescheduleTap: () async { onRescheduleTap: () async {
openDoctorScheduleCalendar(); openDoctorScheduleCalendar();
}, },
timerWidget: Column( timerWidget: (_timeRemaining == null || _timeRemaining == Duration.zero)
children: [ ? null
SizedBox( : Column(
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: [ children: [
Expanded( SizedBox(height: 16.w),
child: _buildTimeUnit( Divider(height: 1, color: AppColors.dividerColor),
_timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00', SizedBox(height: 16.w),
LocaleKeys.days.tr(context: context), Directionality(
), textDirection: ui.TextDirection.ltr,
), child: Row(
_buildTimeSeparator(), mainAxisAlignment: MainAxisAlignment.center,
Expanded( crossAxisAlignment: CrossAxisAlignment.start,
child: _buildTimeUnit( children: [
_timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00', Expanded(
LocaleKeys.hours.tr(context: context), child: _buildTimeUnit(
), _timeRemaining!.inDays.toString().padLeft(2, '0'),
), LocaleKeys.days.tr(context: context),
_buildTimeSeparator(), ),
Expanded( ),
child: _buildTimeUnit( _buildTimeSeparator(),
_timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00', Expanded(
LocaleKeys.minutes.tr(context: context), child: _buildTimeUnit(
), _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0'),
), LocaleKeys.hours.tr(context: context),
_buildTimeSeparator(), ),
Expanded( ),
child: _buildTimeUnit( _buildTimeSeparator(),
_timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00', Expanded(
LocaleKeys.seconds.tr(context: context), 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), SizedBox(height: 16.h),
@ -603,6 +603,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
), ),
const Spacer(), const Spacer(),
BellAnimatedSwitch( BellAnimatedSwitch(
key: _bellSwitchKey,
initialValue: widget.patientAppointmentHistoryResponseModel.hasReminder ?? false, initialValue: widget.patientAppointmentHistoryResponseModel.hasReminder ?? false,
activeColor: AppColors.successColor.withOpacity(0.2), activeColor: AppColors.successColor.withOpacity(0.2),
inactiveColor: AppColors.lightGrayBGColor, inactiveColor: AppColors.lightGrayBGColor,
@ -618,7 +619,9 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
if (newValue == true) { if (newValue == true) {
DateTime startDate = DateTime.now(); DateTime startDate = DateTime.now();
DateTime endDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate); 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, context,
endDate, endDate,
widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "", 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 { } else {
isEventAddedOrRemoved = await calender.checkAndRemove( isEventAddedOrRemoved = await calender.checkAndRemove(
id: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}", id: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}",
@ -1210,8 +1218,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction); handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction);
}, },
backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction), backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction),
borderColor: borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white, textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white,
fontSize: 16.f, fontSize: 16.f,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -1392,9 +1399,10 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Lottie.asset(AppAnimations.warningAnimation, Lottie.asset(AppAnimations.warningAnimation, repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(
SizedBox(height: 12,), height: 12,
),
LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(color: AppColors.textColor, isCenter: true), LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(color: AppColors.textColor, isCenter: true),
SizedBox(height: 24.h), SizedBox(height: 24.h),
// // Countdown Timer - DD : HH : MM : SS format with labels - Always LTR // // Countdown Timer - DD : HH : MM : SS format with labels - Always LTR

@ -33,10 +33,10 @@ class BellAnimatedSwitch extends StatefulWidget {
}); });
@override @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 bool _isActive;
late AnimationController _animationController; late AnimationController _animationController;
late Animation<Alignment> _circleAlignment; late Animation<Alignment> _circleAlignment;
@ -51,11 +51,7 @@ class _BellAnimatedSwitchState extends State<BellAnimatedSwitch> with SingleTick
vsync: this, 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), CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
); );
@ -78,6 +74,24 @@ class _BellAnimatedSwitchState extends State<BellAnimatedSwitch> with SingleTick
super.dispose(); 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() { void _toggle() {
setState(() { setState(() {
_isActive = !_isActive; _isActive = !_isActive;
@ -106,17 +120,19 @@ class _BellAnimatedSwitchState extends State<BellAnimatedSwitch> with SingleTick
Widget _getIcon() { Widget _getIcon() {
if (_isActive) { if (_isActive) {
return widget.activeIcon ?? Icon( return widget.activeIcon ??
Icons.check, Icon(
size: 18, Icons.check,
color: widget.activeIconColor ?? Colors.green.shade400, size: 18,
); color: widget.activeIconColor ?? Colors.green.shade400,
);
} }
return widget.inactiveIcon ?? Icon( return widget.inactiveIcon ??
Icons.close, Icon(
size: 18, Icons.close,
color: widget.inactiveIconColor ?? Colors.grey.shade600, size: 18,
); color: widget.inactiveIconColor ?? Colors.grey.shade600,
);
} }
@override @override
@ -162,9 +178,7 @@ class _BellAnimatedSwitchState extends State<BellAnimatedSwitch> with SingleTick
), ),
], ],
), ),
child: Center( child: Center(child: _getIcon()),
child: _getIcon(),
),
), ),
), ),
], ],

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/app_export.dart';
import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/core/utils/validation_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -138,30 +139,33 @@ class PatientInformationStepState extends State<PatientInformationStep> {
} }
Widget _buildPatientPhoneField(ReferralFormManager formManager) { Widget _buildPatientPhoneField(ReferralFormManager formManager) {
return Focus( return Directionality(
focusNode: _phoneFocusNode, textDirection: ui.TextDirection.ltr,
child: TextInputWidget( child: Focus(
labelText: LocaleKeys.phoneNumber.tr(context: context), focusNode: _phoneFocusNode,
hintText: "5xxxxxxxx", child: TextInputWidget(
controller: _phoneController, labelText: LocaleKeys.phoneNumber.tr(context: context),
padding: const EdgeInsets.all(8), hintText: "5xxxxxxxx",
keyboardType: TextInputType.number, controller: _phoneController,
fontFamily: "Poppins", padding: const EdgeInsets.all(8),
onChange: (value) { keyboardType: TextInputType.number,
formManager.updatePatientPhone(value ?? ''); fontFamily: "Poppins",
}, onChange: (value) {
onCountryChange: (value) { formManager.updatePatientPhone(value ?? '');
formManager.updatePatientCountryEnum(value); },
}, onCountryChange: (value) {
prefix: '966', formManager.updatePatientCountryEnum(value);
isBorderAllowed: false, },
isAllowLeadingIcon: true, prefix: '966',
fontSize: 13, isBorderAllowed: false,
isCountryDropDown: true, isAllowLeadingIcon: true,
leadingIcon: AppAssets.smart_phone, fontSize: 13,
errorMessage: formManager.errors.patientPhone, isCountryDropDown: true,
hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientPhone) leadingIcon: AppAssets.smart_phone,
).paddingSymmetrical(0, 8), errorMessage: formManager.errors.patientPhone,
hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.patientPhone)
).paddingSymmetrical(0, 8),
),
); );
} }

@ -251,7 +251,7 @@ class _AddHealthTrackerEntryPageState extends State<AddHealthTrackerEntryPage> {
_showSelectionBottomSheet( _showSelectionBottomSheet(
context: context, context: context,
title: LocaleKeys.selectMeasureTime.tr(context: context), title: LocaleKeys.selectMeasureTime.tr(context: context),
items: viewModel.bloodSugarMeasureTimeEnList, items: viewModel.getBloodSugarTimeList(),
selectedValue: viewModel.selectedBloodSugarMeasureTime, selectedValue: viewModel.selectedBloodSugarMeasureTime,
onSelected: viewModel.setBloodSugarMeasureTime, onSelected: viewModel.setBloodSugarMeasureTime,
useUpperCase: false, useUpperCase: false,

@ -450,16 +450,16 @@ class _FamilyCardWidgetState extends State<FamilyCardWidget> {
}); });
} }
@override // @override
void didChangeDependencies() { // void didChangeDependencies() {
super.didChangeDependencies(); // super.didChangeDependencies();
// Also sync when dependencies change (e.g., after user switch) // // Also sync when dependencies change (e.g., after user switch)
WidgetsBinding.instance.addPostFrameCallback((_) { // WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { // if (mounted) {
_syncProfileImageFromAppState(); // _syncProfileImageFromAppState();
} // }
}); // });
} // }
void _syncProfileImageFromAppState() { void _syncProfileImageFromAppState() {
try { try {

@ -1,8 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_patient_app_new/core/app_assets.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_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/app_state.dart';
@ -29,16 +31,24 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
final AppState _appState = getIt.get<AppState>(); final AppState _appState = getIt.get<AppState>();
File? _selectedImage; File? _selectedImage;
int? _currentPatientId; int? _currentPatientId;
bool _isInitialLoadTriggered = false;
/// Cache decoded image bytes to avoid decoding base64 on every rebuild
Uint8List? _cachedImageBytes;
String? _cachedImageDataHash;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_currentPatientId = _appState.getAuthenticatedUser()?.patientId; _currentPatientId = _appState.getAuthenticatedUser()?.patientId;
// Pre-cache existing image data if available (prevents blink from default loaded)
_tryCacheExistingImage();
// Use addPostFrameCallback to ensure widget is built before loading // Use addPostFrameCallback to ensure widget is built before loading
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted || _isInitialLoadTriggered) return;
_isInitialLoadTriggered = true;
final patientID = _appState.getAuthenticatedUser()?.patientId; final patientID = _appState.getAuthenticatedUser()?.patientId;
@ -47,16 +57,19 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
return; return;
} }
// Always load fresh data from API - do NOT use AppState cache // Load fresh data from API
print('📥 Loading fresh profile image from API for patient: $patientID'); print('📥 Loading fresh profile image from API for patient: $patientID');
_loadProfileImage(forceRefresh: true); _loadProfileImage(forceRefresh: false);
}); });
} }
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_checkAndUpdateUserImage(); // Only check for user switch, NOT on initial load (initState handles that)
if (_isInitialLoadTriggered) {
_checkAndUpdateUserImage();
}
} }
@override @override
@ -65,6 +78,20 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
_checkAndUpdateUserImage(); _checkAndUpdateUserImage();
} }
/// Pre-cache already-loaded image bytes so we don't flash default avatar
void _tryCacheExistingImage() {
final imageData = _appState.getProfileImageData;
if (imageData != null && imageData.isNotEmpty) {
try {
_cachedImageBytes = base64Decode(imageData);
_cachedImageDataHash = '${imageData.length}_${imageData.hashCode}';
} catch (_) {
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
}
}
void _checkAndUpdateUserImage() { void _checkAndUpdateUserImage() {
// Check if the authenticated user has changed (family member switch) // Check if the authenticated user has changed (family member switch)
final currentPatientId = _appState.getAuthenticatedUser()?.patientId; final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
@ -88,6 +115,10 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
// Then clear ViewModel cache // Then clear ViewModel cache
profileVm.clearProfileImageCache(); profileVm.clearProfileImageCache();
// Clear local decoded bytes cache
_cachedImageBytes = null;
_cachedImageDataHash = null;
// Force rebuild to show default avatar immediately // Force rebuild to show default avatar immediately
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -103,6 +134,7 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
onSuccess: (data) { onSuccess: (data) {
print('✅ Profile image loaded successfully for user: $currentPatientId'); print('✅ Profile image loaded successfully for user: $currentPatientId');
if (mounted) { if (mounted) {
_tryCacheExistingImage();
setState(() {}); // Force rebuild to show new data setState(() {}); // Force rebuild to show new data
} }
}, },
@ -136,7 +168,10 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
forceRefresh: forceRefresh, forceRefresh: forceRefresh,
onSuccess: (data) { onSuccess: (data) {
print('✅ Profile image loaded successfully'); print('✅ Profile image loaded successfully');
// Image loaded successfully if (mounted) {
_tryCacheExistingImage();
setState(() {}); // Rebuild with new cached bytes
}
}, },
onError: (error) { onError: (error) {
print('❌ Error loading profile image: $error'); print('❌ Error loading profile image: $error');
@ -437,6 +472,8 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
imageData: base64String, imageData: base64String,
onSuccess: (data) { onSuccess: (data) {
if (mounted) { if (mounted) {
// Update cached bytes immediately from the uploaded data
_tryCacheExistingImage();
setState(() { setState(() {
_selectedImage = null; // Clear selected image after successful upload _selectedImage = null; // Clear selected image after successful upload
}); });
@ -481,80 +518,84 @@ class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
); );
} }
// // IMPORTANT: Verify the cached image belongs to the current user // Use cached decoded bytes update cache if source data changed
// // If _currentPatientId doesn't match currentPatientId, don't show cached image final String? imageData = GetIt.instance<AppState>().getProfileImageData;
// if (currentPatientId != _currentPatientId) { final String? currentHash = (imageData != null && imageData.isNotEmpty)
// print('⚠️ Patient ID mismatch - showing default avatar (current: $currentPatientId, cached: $_currentPatientId)'); ? '${imageData.length}_${imageData.hashCode}'
// return Image.asset( : null;
// defaultImage,
// width: 136.w,
// height: 136.h,
// );
// }
// IMPORTANT: Use ONLY ViewModel data, DO NOT fallback to AppState
// This prevents showing stale cached data when switching users
final String? imageData = profileVm.profileImageData;
print('🖼️ Building profile image - has data: ${imageData != null && imageData.isNotEmpty}, patient: $currentPatientId (ViewModel ONLY)'); // Re-decode only if the underlying data actually changed
if (currentHash != null && currentHash != _cachedImageDataHash) {
// Show uploaded image if available
if (imageData != null && imageData.isNotEmpty) {
try { try {
final bytes = base64Decode(imageData); _cachedImageBytes = base64Decode(imageData!);
return ClipOval( _cachedImageDataHash = currentHash;
child: Image.memory(
bytes,
width: 136.w,
height: 136.h,
fit: BoxFit.cover,
),
);
} catch (e) { } catch (e) {
print('❌ Error decoding profile image: $e'); print('❌ Error decoding profile image: $e');
// If decoding fails, show default image _cachedImageBytes = null;
return Image.asset( _cachedImageDataHash = null;
defaultImage, }
} else if (currentHash == null) {
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
// Show cached decoded image if available
if (_cachedImageBytes != null) {
return ClipOval(
child: Image.memory(
_cachedImageBytes!,
key: ValueKey('profile_$currentPatientId'),
width: 136.w, width: 136.w,
height: 136.h, height: 136.h,
); fit: BoxFit.cover,
} gaplessPlayback: true, // Prevents blink during image rebuild
),
);
} }
// Show default image (no image data or user has no uploaded image) // Show default image (no image data or user has no uploaded image)
print('📷 Showing default avatar for user $currentPatientId'); print('📷 Showing default avatar for user $currentPatientId');
return Image.asset( return Image.asset(
defaultImage, defaultImage,
key: ValueKey('default_$currentPatientId'),
width: 136.w, width: 136.w,
height: 136.h, height: 136.h,
gaplessPlayback: true,
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Check for user change on every build (handles navigation scenarios) // Removed addPostFrameCallback from build() it was causing redundant
WidgetsBinding.instance.addPostFrameCallback((_) { // _checkAndUpdateUserImage calls on every single rebuild.
if (mounted) { // didChangeDependencies + didUpdateWidget already handle user switches.
_checkAndUpdateUserImage();
}
});
return Consumer<ProfileSettingsViewModel>( return Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) { builder: (context, profileVm, _) {
// If we already have cached bytes, show the image even while "loading"
// to prevent the shimmerimage blink on page open
final bool showShimmer = profileVm.isProfileImageLoading &&
_cachedImageBytes == null &&
_selectedImage == null;
return Center( return Center(
child: Stack( child: Stack(
children: [ children: [
// Profile Image // Profile Image use AnimatedSwitcher to smooth transition
profileVm.isProfileImageLoading AnimatedSwitcher(
? Container( duration: const Duration(milliseconds: 200),
width: 136.w, child: showShimmer
height: 136.h, ? Container(
decoration: BoxDecoration( key: const ValueKey('shimmer'),
shape: BoxShape.circle, width: 136.w,
color: AppColors.greyTextColor.withValues(alpha: 0.2), height: 136.h,
), decoration: BoxDecoration(
).toShimmer2(isShow: true) shape: BoxShape.circle,
: _buildProfileImage(profileVm), color: AppColors.greyTextColor.withValues(alpha: 0.2),
),
).toShimmer2(isShow: true)
: _buildProfileImage(profileVm),
),
// Edit button // Edit button
Positioned( Positioned(

@ -17,82 +17,92 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class BottomSheetUtils { 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 { {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) { if (Platform.isAndroid) {
// Android: Check existing permission first using PermissionService
bool hasPermission = await PermissionService.isCalendarPermissionEnabled(); bool hasPermission = await PermissionService.isCalendarPermissionEnabled();
if (!hasPermission) {
PermissionStatus status = await Permission.calendarFullAccess.request();
hasPermission = status.isGranted;
}
if (hasPermission) { if (hasPermission) {
_showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, return true;
onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); }
} else {
showCommonBottomSheetWithoutHeight( // Request calendarFullAccess permission (Android 14+ requires this)
title: LocaleKeys.notice.tr(context: GetIt.instance<NavigationService>().navigatorKey.currentContext!), // This internally requests both READ_CALENDAR and WRITE_CALENDAR
GetIt.instance<NavigationService>().navigatorKey.currentContext!, PermissionStatus status = await Permission.calendarFullAccess.request();
child: Utils.getWarningWidget(
loadingText: LocaleKeys.calendarPermissionAlert.tr(), // If calendarFullAccess request didn't show dialog or failed,
isShowActionButtons: true, // fallback to requesting individual permissions
onCancelTap: () { if (!status.isGranted) {
GetIt.instance<NavigationService>().pop(); Map<Permission, PermissionStatus> statuses = await [
}, Permission.calendarWriteOnly,
onConfirmTap: () async { ].request();
GetIt.instance<NavigationService>().pop();
openAppSettings(); return statuses[Permission.calendarWriteOnly]?.isGranted ?? false;
}),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
} }
return status.isGranted;
} else { } else {
if (await Permission.calendarWriteOnly.request().isGranted) { // iOS: Check if we already have full access
if (await Permission.calendarFullAccess.request().isGranted) { PermissionStatus fullAccessStatus = await Permission.calendarFullAccess.status;
_showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted,
onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); if (fullAccessStatus.isGranted) {
} else { return true;
showCommonBottomSheetWithoutHeight( }
title: LocaleKeys.notice.tr(context: GetIt.instance<NavigationService>().navigatorKey.currentContext!),
GetIt.instance<NavigationService>().navigatorKey.currentContext!, // Request write permission first (required before full access on iOS)
child: Utils.getWarningWidget( PermissionStatus writeStatus = await Permission.calendarWriteOnly.request();
loadingText: LocaleKeys.calendarPermissionAlert.tr(), if (!writeStatus.isGranted) {
isShowActionButtons: true, return false;
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,
);
} }
// 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, 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 { {required Function onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool? isMultiAllowed}) async {
showCommonBottomSheetWithoutHeight(providedContext, title: LocaleKeys.setTimerOfReminder.tr(context: providedContext), child: PrescriptionReminderView( showCommonBottomSheetWithoutHeight(providedContext, title: LocaleKeys.setTimerOfReminder.tr(context: providedContext), child: PrescriptionReminderView(

Loading…
Cancel
Save