Merge branch 'refs/heads/master' into offers_discounts

# Conflicts:
#	lib/presentation/my_family/widget/family_cards.dart
pull/319/head
Aamir Muhammad 1 month ago
commit 4fd2312e61

File diff suppressed because it is too large Load Diff

@ -0,0 +1,444 @@
# Theme Implementation Guide - HMG Patient App
## Overview
This Flutter application implements a comprehensive **dual-theme system** supporting both **Light Mode** and **Dark Mode**. The theme system is built with a combination of Flutter's native theming and a custom color management system.
---
## Architecture
### 1. **Core Components**
#### A. AppTheme Class (`lib/theme/app_theme.dart`)
- **Purpose**: Defines the Material theme configurations for both light and dark modes
- **Key Methods**:
- `getTheme(isArabic)` - Returns light theme configuration
- `getDarkTheme(isArabic)` - Returns dark theme configuration
**Features**:
- Dynamic font family selection based on locale (Arabic: `CairoArabic`, English: `Poppins`)
- Platform-specific page transitions (Android: Zoom, iOS: Cupertino)
- Consistent styling for AppBar, BottomSheet, FloatingActionButton
- Transparent splash colors for better UX
- System overlay styles (Dark for light theme, Light for dark theme)
#### B. AppColors Class (`lib/theme/colors.dart`)
- **Purpose**: Centralized color management with dual-palette support
- **Architecture**: Three-tier color system
**Three Access Patterns**:
1. **Static Getters (Global State-Based)**
```dart
AppColors.primaryRedColor // Uses AppColors.isDarkMode flag
```
- Checks global `AppColors.isDarkMode` boolean
- Returns dark or light palette value accordingly
2. **AppColorsDark Class (Dark Palette)**
```dart
AppColors.dark.primaryRedColor // Always dark variant
```
- Constant dark mode color values
- Used as source for dark theme colors
3. **BuildContext Extension (Theme-Aware)**
```dart
context.primaryRedColor // Uses Theme brightness
```
- Reads `Theme.of(context).brightness`
- Automatically adapts to MaterialApp's themeMode
---
## 2. **Theme State Management**
### ProfileSettingsViewModel
Located in `lib/features/profile_settings/profile_settings_view_model.dart`
**Responsibilities**:
- Manages dark mode state (`_isDarkMode` boolean)
- Persists preference to local storage (CacheService)
- Triggers UI rebuilds on theme changes
**Key Methods**:
```dart
// Called at app startup (before first frame)
void loadDarkMode() {
final saved = _cacheService.getBool(key: _darkModeKey);
_isDarkMode = saved ?? false;
AppColors.isDarkMode = _isDarkMode;
// No notifyListeners() - called before build
}
// Toggle theme and persist
void toggleDarkMode(bool value) {
_isDarkMode = value;
AppColors.isDarkMode = value;
_cacheService.saveBool(key: _darkModeKey, value: value);
notifyListeners(); // Triggers rebuild
}
```
---
## 3. **Application Lifecycle**
### Initialization Flow (`main.dart`)
```dart
Future<void> callInitializations() async {
// ... Firebase, dependencies setup ...
// Restore dark mode BEFORE first frame
getIt.get<ProfileSettingsViewModel>().loadDarkMode();
}
```
### Theme Application in Widget Tree
```dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar";
return MaterialApp(
key: ValueKey('app_theme_${profileVm.isDarkMode}'), // Force rebuild on theme change
theme: AppTheme.getTheme(isArabic),
darkTheme: AppTheme.getDarkTheme(isArabic),
themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light,
// ... other properties
);
},
);
}
}
```
**Key Mechanism**:
- `Consumer<ProfileSettingsViewModel>` listens for theme changes
- When `toggleDarkMode()` is called → `notifyListeners()` → Widget rebuilds
- `themeMode` property switches between `ThemeMode.dark` and `ThemeMode.light`
- `ValueKey` forces MaterialApp to rebuild entirely on theme change
---
## 4. **Color System Design**
### Dual-Palette Architecture
```dart
class AppColors {
static bool isDarkMode = false; // Global flag
// Pattern: Conditional getter
static Color get primaryRedColor =>
isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B);
// Dark palette instance
static const AppColorsDark dark = AppColorsDark();
}
class AppColorsDark {
const AppColorsDark();
// Dark mode variants
Color get primaryRedColor => const Color(0xFFDE5C5D);
Color get scaffoldBgColor => const Color(0xFF191919);
Color get textColor => const Color(0xFFFFFFFF);
// ... 100+ color definitions
}
```
### BuildContext Extension for Theme-Aware Colors
```dart
extension AppColorsContext on BuildContext {
bool get _isDark => Theme.of(this).brightness == Brightness.dark;
Color get scaffoldBgColor =>
_isDark ? AppColors.dark.scaffoldBgColor : const Color(0xFFF8F8F8);
Color get textColor =>
_isDark ? AppColors.dark.textColor : const Color(0xFF2E3039);
}
```
**Usage in Widgets**:
```dart
// Option 1: Static (uses global flag)
color: AppColors.primaryRedColor
// Option 2: Extension (uses Theme.of(context))
color: context.primaryRedColor
// Option 3: Direct dark access
color: AppColors.dark.primaryRedColor
```
---
## 5. **User Interface Implementation**
### Theme Toggle Control (`profile_settings.dart`)
```dart
Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
return actionItem(
AppAssets.darkModeIcon,
LocaleKeys.darkMode.tr(context: context),
() {
profileVm.toggleDarkMode(!profileVm.isDarkMode);
},
switchValue: profileVm.isDarkMode,
onSwitchChanged: (value) {
profileVm.toggleDarkMode(value);
},
);
},
)
```
---
## 6. **Color Categories**
### Organized Color Groups
| Category | Light Example | Dark Example |
|----------|---------------|--------------|
| **Scaffold/Background** | `0xFFF8F8F8` | `0xFF191919` |
| **Primary Brand** | `0xFFED1C2B` (Red) | `0xFFDE5C5D` (Softer Red) |
| **Text** | `0xFF2E3039` (Dark) | `0xFFFFFFFF` (White) |
| **Success** | `0xFF18C273` (Green) | `0xFF18C273` (Same) |
| **Error** | `0xFFED1C2B` | `0xFFD63D48` |
| **Card Surface** | `0xFFFFFFFF` | `0xFF1E1E1E` |
| **Borders** | `0x332E3039` | `0x55ECECEC` |
| **Shimmer Base** | `0xFFE0E0E0` | `0xFF2C2C2C` |
### Special Purpose Colors
- **Rating Stars**: Constant across themes (`0xFFFFA726`)
- **Health Calculators**: Feature-specific colors maintained in both themes
- **Status Colors**: Pending, Processing, Completed, Rejected
- **Info Banners**: Warning backgrounds with adjusted opacity for dark mode
---
## 7. **Best Practices for Developers**
### Adding New Colors
1. **Define in Light Theme**:
```dart
static Color get newFeatureColor =>
isDarkMode ? dark.newFeatureColor : const Color(0xFFXXXXXX);
```
2. **Add Dark Variant**:
```dart
class AppColorsDark {
Color get newFeatureColor => const Color(0xFFYYYYYY);
}
```
3. **Optional: Add to BuildContext Extension**:
```dart
extension AppColorsContext on BuildContext {
Color get newFeatureColor =>
_isDark ? AppColors.dark.newFeatureColor : const Color(0xFFXXXXXX);
}
```
### Using Colors in Widgets
**Recommended Approach**:
```dart
// For colors that should adapt to theme
Container(
color: AppColors.scaffoldBgColor, // Uses global isDarkMode
)
// Or using context extension
Container(
color: context.scaffoldBgColor, // Uses Theme.brightness
)
```
**Avoid**:
```dart
// Don't hardcode colors
Container(
color: Color(0xFFED1C2B), // ❌ Won't adapt to dark mode
)
```
### Testing Theme Changes
```dart
// Toggle theme programmatically
context.read<ProfileSettingsViewModel>().toggleDarkMode(true);
// Check current state
final isDark = context.read<ProfileSettingsViewModel>().isDarkMode;
```
---
## 8. **Persistence Layer**
### CacheService Integration
- Uses shared_preferences or similar local storage
- Key: `_darkModeKey` (defined in ProfileSettingsViewModel)
- Automatically loads on app startup
- Saves immediately on toggle
**Flow**:
1. App launches → `loadDarkMode()` reads from cache
2. User toggles → `toggleDarkMode(value)` saves to cache
3. App restart → Previous preference restored
---
## 9. **Font Handling**
### Locale-Specific Fonts
```dart
// In AppTheme
fontFamily: isArabic ? 'CairoArabic' : 'Poppins'
textTheme: const TextTheme(
displayLarge: TextStyle(fontFamily: 'CairoArabic'),
bodyLarge: TextStyle(fontFamily: 'CairoArabic'),
)
```
**Rationale**: Arabic text requires specialized fonts for proper rendering
---
## 10. **System Integration**
### AppBar System Overlay
```dart
// Light Theme
systemOverlayStyle: SystemUiOverlayStyle.dark // Dark status bar icons
// Dark Theme
systemOverlayStyle: SystemUiOverlayStyle.light // Light status bar icons
```
### Platform-Specific Adjustments
```dart
// SafeArea handling
SafeArea(
top: false,
bottom: Platform.isIOS ? false : true,
)
```
---
## 11. **Advanced Features**
### Gradient Support
```dart
static const LinearGradient aiLinearGradient = LinearGradient(
colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
```
*Note: Gradients remain constant across themes for brand consistency*
### Transparency Management
```dart
static const transparent = Colors.transparent;
// Used for splash colors, bottomSheet backgrounds
```
---
## 12. **Common Pitfalls & Solutions**
### Issue 1: Colors Not Updating
**Problem**: Widget doesn't reflect theme change
**Solution**: Ensure widget rebuilds on theme change
```dart
// Wrap with Consumer or use context.watch
Consumer<ProfileSettingsViewModel>(
builder: (context, vm, _) => YourWidget(),
)
```
### Issue 2: Inconsistent Colors
**Problem**: Some UI elements use wrong theme
**Solution**: Always use `AppColors.*` or `context.*`, never hardcode
### Issue 3: Theme Not Persisting
**Problem**: Theme resets on app restart
**Solution**: Verify `loadDarkMode()` is called in `callInitializations()`
---
## 13. **File Structure**
```
lib/
├── theme/
│ ├── app_theme.dart # Theme configurations
│ └── colors.dart # Color definitions & dark palette
├── features/
│ └── profile_settings/
│ └── profile_settings_view_model.dart # Theme state management
├── presentation/
│ └── profile_settings/
│ └── profile_settings.dart # UI with theme toggle
└── main.dart # Theme initialization & application
```
---
## 14. **Summary**
### Workflow
1. **Initialization**: Load saved theme preference → Set `AppColors.isDarkMode`
2. **User Action**: Toggle switch in Settings → Call `toggleDarkMode()`
3. **State Update**: Update flag → Save to cache → Notify listeners
4. **UI Rebuild**: Consumer rebuilds → MaterialApp switches `themeMode`
5. **Color Resolution**: All `AppColors.*` getters return appropriate palette
### Key Advantages
**Centralized**: Single source of truth for colors
**Type-Safe**: Compile-time color checking
**Persistent**: Survives app restarts
**Flexible**: Multiple access patterns for different use cases
**Maintainable**: Easy to add/modify colors
**Performance**: No runtime color calculations, just conditional returns
---
## 15. **Migration Checklist for New Features**
When adding a new screen/feature:
- [ ] Use `AppColors.*` for all color references
- [ ] Test in both light and dark modes
- [ ] Ensure text contrast meets accessibility standards
- [ ] Add dark variants for any new colors
- [ ] Verify with Arabic locale (font rendering)
- [ ] Check shimmer/loading states in both themes
- [ ] Test on both iOS and Android
---
**Last Updated**: January 2024
**Maintained By**: HMG Development Team

@ -76,6 +76,7 @@ class CacheConst {
static const String hasEnabledQuickLogin = 'has-enabled-quick-login'; static const String hasEnabledQuickLogin = 'has-enabled-quick-login';
static const String quickLoginEnabled = 'quick-login-enabled'; static const String quickLoginEnabled = 'quick-login-enabled';
static const String isMonthlyReportEnabled = 'is-monthly-report-enabled'; static const String isMonthlyReportEnabled = 'is-monthly-report-enabled';
static const String isShowSymptomCheckerBottomSheet = 'is-show-symptom-checker-bottom-sheet';
static const String zoomRoomID = 'zoom-room-id'; static const String zoomRoomID = 'zoom-room-id';
static const String callTypeID = 'call-type-id'; static const String callTypeID = 'call-type-id';

@ -617,7 +617,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
patientType: patientType); patientType: patientType);
result.fold( result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure), // (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) { (apiResponse) {
if (apiResponse.messageStatus == 2) { if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});

@ -862,6 +862,16 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
}); });
} }
}); });
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}); });
}, onError: (err) { }, onError: (err) {
showCommonBottomSheetWithoutHeight( showCommonBottomSheetWithoutHeight(

@ -6,6 +6,7 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.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';
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/dependencies.dart';
import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/location_util.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
@ -132,12 +133,12 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Image.network( Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 64.h, width: 64.h,
height: 64.h, height: 64.h,
fit: BoxFit.cover, fit: BoxFit.cover,
).circle(100).toShimmer2(isShow: true, radius: 50.r), ).circle(100).toShimmer2(isShow: true, radius: 50.r),
SizedBox(height: 8.h), SizedBox(height: 8.h),
("Dr. John").toString().toText12(isBold: true, isCenter: true, maxLine: 2).toShimmer2(isShow: true), ("Dr. John").toString().toText12(isBold: true, isCenter: true, maxLine: 2).toShimmer2(isShow: true),
], ],
); );
@ -337,96 +338,99 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
], ],
) )
: Column( : Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 24.h), SizedBox(height: 24.h),
LocaleKeys.favourites.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.w, 0.h), LocaleKeys.favourites.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.w, 0.h),
SizedBox(height: 16.h), SizedBox(height: 16.h),
SizedBox( SizedBox(
height: 110.h, height: 110.h,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: myAppointmentsVM.patientFavouriteDoctorsList.length + 1, itemCount: myAppointmentsVM.patientFavouriteDoctorsList.length + 1,
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.only(left: 24.w, right: 24.w), padding: EdgeInsets.only(left: 24.w, right: 24.w),
itemBuilder: (context, index) { itemBuilder: (context, index) {
// Last item: static "Add" button // Last item: static "Add" button
if (index == myAppointmentsVM.patientFavouriteDoctorsList.length) { if (index == myAppointmentsVM.patientFavouriteDoctorsList.length) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Utils.buildSvgWithAssets(icon: AppAssets.add_new_family_icon, height: 64.h, width: 64.h, applyThemeColor: false), Utils.buildSvgWithAssets(icon: AppAssets.add_new_family_icon, height: 64.h, width: 64.h, applyThemeColor: false),
SizedBox( SizedBox(
width: 80.w, width: 80.w,
child: LocaleKeys.add.tr(context: context).toText12( child: LocaleKeys.add
.tr(context: context)
.toText12(
color: AppColors.textColor, color: AppColors.textColor,
isBold: true, isBold: true,
isCenter: true, isCenter: true,
).paddingOnly(top: 4.h), )
), .paddingOnly(top: 4.h),
], ),
).onPress(() { ],
Navigator.of(context).push(CustomPageRoute(page: SearchDoctorByName())); ).onPress(() {
}); Navigator.of(context).push(CustomPageRoute(page: SearchDoctorByName()));
} });
return AnimationConfiguration.staggeredList( }
position: index, return AnimationConfiguration.staggeredList(
duration: const Duration(milliseconds: 1000), position: index,
child: SlideAnimation( duration: const Duration(milliseconds: 1000),
horizontalOffset: 100.0, child: SlideAnimation(
child: FadeInAnimation( horizontalOffset: 100.0,
child: SizedBox( child: FadeInAnimation(
child: Column( child: SizedBox(
crossAxisAlignment: CrossAxisAlignment.center, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.center,
Image.network( children: [
myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!, Image.network(
width: 64.h, myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!,
height: 64.h, width: 64.h,
fit: BoxFit.cover, height: 64.h,
).circle(100).toShimmer2(isShow: false, radius: 50.r), fit: BoxFit.cover,
SizedBox(height: 8.h), ).circle(100).toShimmer2(isShow: false, radius: 50.r),
SizedBox( SizedBox(height: 8.h),
width: 80.w, SizedBox(
child: (myAppointmentsVM.patientFavouriteDoctorsList[index].doctorName) width: 80.w,
.toString() child: (myAppointmentsVM.patientFavouriteDoctorsList[index].doctorName)
.toText12(isBold: true, isCenter: true, maxLine: 2) .toString()
.toShimmer2(isShow: false), .toText12(isBold: true, isCenter: true, maxLine: 2)
), .toShimmer2(isShow: false),
],
), ),
).onPress(() async { ],
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
clinicID: myAppointmentsVM.patientFavouriteDoctorsList[index].clinicId,
projectID: myAppointmentsVM.patientFavouriteDoctorsList[index].projectId,
doctorID: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorId,
));
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(isDoctorAllowedToBook: true),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}),
), ),
), ).onPress(() async {
); bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
}, clinicID: myAppointmentsVM.patientFavouriteDoctorsList[index].clinicId,
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), projectID: myAppointmentsVM.patientFavouriteDoctorsList[index].projectId,
), doctorID: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorId,
), ));
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(isDoctorAllowedToBook: true),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}),
),
),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h),
),
),
], ],
); );
}), }),
@ -552,96 +556,98 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
], ],
).paddingSymmetrical(24.h, 0.h); ).paddingSymmetrical(24.h, 0.h);
case 1: case 1:
//TODO: Get LiveCare type Select UI from Hussain //TODO: Get LiveCare type Select UI from Hussain
return appState.isAuthenticated return appState.isAuthenticated
? Column( ? Column(
children: [ children: [
Container( Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: 24.h, borderRadius: 24.h,
hasShadow: false, hasShadow: false,
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
children: [ children: [
Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h, applyThemeColor: false), Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 12.h), SizedBox(width: 12.h),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LocaleKeys.immediateConsultation.tr(context: context).toText14(color: AppColors.textColor, isBold: true), LocaleKeys.immediateConsultation.tr(context: context).toText14(color: AppColors.textColor, isBold: true),
LocaleKeys.tapToSelectClinic.tr(context: context).toText12(color: AppColors.primaryRedColor, isBold: true), LocaleKeys.tapToSelectClinic.tr(context: context).toText12(color: AppColors.primaryRedColor, isBold: true),
], ],
), ),
], ],
), ),
Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), Transform.flip(
], flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)),
).onPress(() async { ],
//TODO Implement API to check for existing LiveCare Requests ).onPress(() async {
//TODO Implement API to check for existing LiveCare Requests
LoaderBottomSheet.showLoader(); LoaderBottomSheet.showLoader();
await immediateLiveCareViewModel.getPatientLiveCareHistory(); await immediateLiveCareViewModel.getPatientLiveCareHistory();
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) { if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) {
Navigator.of(context).push( Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(
page: ImmediateLiveCarePendingRequestPage(), page: ImmediateLiveCarePendingRequestPage(),
), ),
); );
} else { } else {
Navigator.of(context).push( Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(
page: SelectImmediateLiveCareClinicPage(), page: SelectImmediateLiveCareClinicPage(),
), ),
); );
} }
}), }),
SizedBox(height: 16.h), SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
SizedBox(height: 16.h), SizedBox(height: 16.h),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
children: [ children: [
Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h, applyThemeColor: false), Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h, applyThemeColor: false),
SizedBox(width: 12.h), SizedBox(width: 12.h),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LocaleKeys.scheduledConsultation.tr(context: context).toText14(color: AppColors.textColor, isBold: true), LocaleKeys.scheduledConsultation.tr(context: context).toText14(color: AppColors.textColor, isBold: true),
LocaleKeys.tapToSelectClinic.tr(context: context).toText12(color: AppColors.primaryRedColor, isBold: true), LocaleKeys.tapToSelectClinic.tr(context: context).toText12(color: AppColors.primaryRedColor, isBold: true),
], ],
), ),
], ],
), ),
Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), Transform.flip(
], flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)),
).onPress(() { ],
bookAppointmentsViewModel.setIsClinicsListLoading(true); ).onPress(() {
bookAppointmentsViewModel.setIsLiveCareSchedule(true); bookAppointmentsViewModel.setIsClinicsListLoading(true);
Navigator.of(context).push( bookAppointmentsViewModel.setIsLiveCareSchedule(true);
CustomPageRoute( Navigator.of(context).push(
page: SelectClinicPage(), CustomPageRoute(
), page: SelectClinicPage(),
); ),
}), );
], }),
), ],
), ),
), ),
], ),
).paddingSymmetrical(24.h, 0.h) ],
).paddingSymmetrical(24.h, 0.h)
: getLiveCareNotLoggedInUI(); : getLiveCareNotLoggedInUI();
default: default:
SizedBox.shrink(); SizedBox.shrink();
@ -657,28 +663,28 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LocaleKeys.notSureHelpMeChooseClinic.tr(context: context).toText16(isBold: true, color: AppColors.textColor), LocaleKeys.notSureHelpMeChooseClinic.tr(context: context).toText16(isBold: true, color: AppColors.textColor),
SizedBox(height: 8.h), SizedBox(height: 8.h),
LocaleKeys.checkYourSymptomsWithScale.tr(context: context).toText12( LocaleKeys.checkYourSymptomsWithScale.tr(context: context).toText12(
isBold: true, isBold: true,
color: AppColors.greyTextColor, color: AppColors.greyTextColor,
),
],
),
), ),
SizedBox(width: 16.w),
CustomButton(
height: 42.h,
width: 42.w,
text: "",
onPressed: () => context.navigateWithName(AppRoutes.userInfoSelection),
icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward,
)
], ],
), ).paddingAll(24.w),
),
SizedBox(width: 16.w),
CustomButton(
height: 42.h,
width: 42.w,
text: "",
onPressed: () => context.navigateWithName(AppRoutes.userInfoSelection),
icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward,
)
],
).paddingAll(24.w),
) )
: SizedBox.shrink(); : SizedBox.shrink();
} }
@ -687,13 +693,10 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
regionalViewModel.flush(); regionalViewModel.flush();
regionalViewModel.setBottomSheetType(type); regionalViewModel.setBottomSheetType(type);
// AppointmentViaRegionViewmodel? viewmodel = null; // AppointmentViaRegionViewmodel? viewmodel = null;
showCommonBottomSheetWithoutHeight(context, title: "", showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer<AppointmentViaRegionViewmodel>(builder: (_, data, __) => getTitle(data)), isDismissible: false,
titleWidget: Consumer<AppointmentViaRegionViewmodel>(builder: (_, data, __) => getTitle(data)),
isDismissible: false,
child: Consumer<AppointmentViaRegionViewmodel>(builder: (_, data, __) { child: Consumer<AppointmentViaRegionViewmodel>(builder: (_, data, __) {
return getRegionalSelectionWidget(data); return getRegionalSelectionWidget(data);
}), }), callBackFunc: () {});
callBackFunc: () {});
} }
Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data) { Widget getRegionalSelectionWidget(AppointmentViaRegionViewmodel data) {
@ -749,7 +752,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
return SizedBox.shrink(); return SizedBox.shrink();
} }
void _handleSortByLocationToggle(bool value, AppointmentViaRegionViewmodel regionVM) { void _handleSortByLocationToggle(bool value, AppointmentViaRegionViewmodel regionVM) {
if (value) { if (value) {
final locationUtils = getIt.get<LocationUtils>(); final locationUtils = getIt.get<LocationUtils>();
locationUtils.getLocation( locationUtils.getLocation(
@ -817,6 +820,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
} }
} }
} }
bookAppointmentsViewModel.addListener(listener); bookAppointmentsViewModel.addListener(listener);
bookAppointmentsViewModel.getRegionMappedProjectList(); bookAppointmentsViewModel.getRegionMappedProjectList();
} }
@ -927,52 +931,58 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
).paddingSymmetrical(24.h, 0.h); ).paddingSymmetrical(24.h, 0.h);
} }
void showUnKnownClinicBottomSheet() { void showUnKnownClinicBottomSheet() async {
showCommonBottomSheetWithoutHeight( if (await Utils.getBoolFromPrefs(CacheConst.isShowSymptomCheckerBottomSheet)) {
context, showCommonBottomSheetWithoutHeight(
title: "", context,
isDismissible: true, title: "",
isDismissible: true,
isCloseButtonVisible: false, isCloseButtonVisible: false,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LocaleKeys.notSureHelpMeChooseClinic.tr(context: context).toText28(color: AppColors.textColor, isBold: true, height: 1.5), LocaleKeys.notSureHelpMeChooseClinic.tr(context: context).toText28(color: AppColors.textColor, isBold: true, height: 1.5),
SizedBox(height: 4.h), SizedBox(height: 4.h),
LocaleKeys.mentionYourSymptomsAndFindDoctors.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true,), LocaleKeys.mentionYourSymptomsAndFindDoctors.tr(context: context).toText12(
SizedBox(height: 24.h), color: AppColors.greyTextColor,
CustomButton( isBold: true,
text: LocaleKeys.yesPleaseINeedHelp.tr(context: context), ),
onPressed: () { SizedBox(height: 24.h),
context.pop(); CustomButton(
context.navigateWithName(AppRoutes.userInfoSelection); text: LocaleKeys.yesPleaseINeedHelp.tr(context: context),
}, onPressed: () {
backgroundColor: AppColors.primaryRedColor, context.pop();
borderColor: AppColors.primaryRedColor, context.navigateWithName(AppRoutes.userInfoSelection);
textColor: AppColors.whiteColor, },
fontSize: 16.f, backgroundColor: AppColors.primaryRedColor,
isBold: true, borderColor: AppColors.primaryRedColor,
borderRadius: 12.r, textColor: AppColors.whiteColor,
// padding: EdgeInsets.fromLTRB(10, 0, 10, 0), fontSize: 16.f,
height: 56.h, isBold: true,
), borderRadius: 12.r,
SizedBox(height: 8.h), // padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
CustomButton( height: 56.h,
text: LocaleKeys.noThanksIKnowTheClinic.tr(context: context), ),
onPressed: () { SizedBox(height: 8.h),
context.pop(); CustomButton(
}, text: LocaleKeys.noThanksIKnowTheClinic.tr(context: context),
backgroundColor: AppColors.chipSecondaryLightRedColor, onPressed: () {
borderColor: AppColors.chipSecondaryLightRedColor, Utils.saveBoolFromPrefs(CacheConst.isShowSymptomCheckerBottomSheet, false);
textColor: AppColors.primaryRedColor, context.pop();
fontSize: 16.f, },
isBold: true, backgroundColor: AppColors.chipSecondaryLightRedColor,
borderRadius: 12.r, borderColor: AppColors.chipSecondaryLightRedColor,
// padding: EdgeInsets.fromLTRB(10, 0, 10, 0), textColor: AppColors.primaryRedColor,
height: 56.h, fontSize: 16.f,
), isBold: true,
], borderRadius: 12.r,
).paddingSymmetrical(24.w, 20.h), // padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
callBackFunc: () {}, height: 56.h,
); ),
],
).paddingSymmetrical(24.w, 20.h),
callBackFunc: () {},
);
}
} }
} }

@ -1,7 +1,10 @@
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
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_state.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/dependencies.dart';
@ -14,6 +17,7 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.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/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart';
@ -41,6 +45,9 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
late MyAppointmentsViewModel myAppointmentsViewModel; late MyAppointmentsViewModel myAppointmentsViewModel;
late SymptomsCheckerViewModel symptomsCheckerViewModel; late SymptomsCheckerViewModel symptomsCheckerViewModel;
Uint8List? _cachedImageBytes;
String? _cachedImageDataHash;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false); bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
@ -156,11 +163,14 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
padding: EdgeInsets.all(16.h), padding: EdgeInsets.all(16.h),
child: Row( child: Row(
children: [ children: [
Image.asset( // Image.asset(
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, // appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 52.h, // width: 52.h,
height: 52.h, // height: 52.h,
), // ),
Consumer<ProfileSettingsViewModel>(builder: (context, profileVm, _) {
return _buildProfileImage(profileVm);
}),
SizedBox(width: 8.h), SizedBox(width: 8.h),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -260,6 +270,79 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
); );
} }
Widget _buildProfileImage(ProfileSettingsViewModel profileVm) {
// Always get fresh user data
final currentUser = appState.getAuthenticatedUser();
final currentPatientId = currentUser?.patientId;
final gender = currentUser?.gender ?? 1;
final age = currentUser?.age ?? 0;
// Determine the default image based on gender and age
final String defaultImage;
if (gender == 1) {
// Male
defaultImage = age < 7 ? AppAssets.babyBoyImg : AppAssets.maleImg;
} else {
// Female
defaultImage = age < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg;
}
// Show selected image if available (only during upload)
// if (_selectedImage != null) {
// return ClipOval(
// child: Image.file(
// _selectedImage!,
// width: 136.w,
// height: 136.h,
// fit: BoxFit.cover,
// ),
// );
// }
// Use cached decoded bytes update cache if source data changed
final String? imageData = GetIt.instance<AppState>().getProfileImageData;
final String? currentHash = (imageData != null && imageData.isNotEmpty) ? '${imageData.length}_${imageData.hashCode}' : null;
// Re-decode only if the underlying data actually changed
if (currentHash != null && currentHash != _cachedImageDataHash) {
try {
_cachedImageBytes = base64Decode(imageData!);
_cachedImageDataHash = currentHash;
} catch (e) {
print('❌ Error decoding profile image: $e');
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
} 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: 52.w,
height: 52.h,
fit: BoxFit.cover,
gaplessPlayback: true, // Prevents blink during image rebuild
),
);
}
// Show default image (no image data or user has no uploaded image)
print('📷 Showing default avatar for user $currentPatientId');
return Image.asset(
defaultImage,
key: ValueKey('default_$currentPatientId'),
width: 52.w,
height: 52.h,
gaplessPlayback: true,
);
}
void getWalkInAppointmentPatientShare() async { void getWalkInAppointmentPatientShare() async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingAppointmentShare.tr(context: context)); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingAppointmentShare.tr(context: context));
await bookAppointmentsViewModel.getWalkInPatientShareAppointment(onSuccess: (val) { await bookAppointmentsViewModel.getWalkInPatientShareAppointment(onSuccess: (val) {

Loading…
Cancel
Save