Merge pull request 'dev_sultan' (#160) from dev_sultan into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/160
haroon_dev
Haroon6138 9 hours ago
commit 23b92cef41

@ -151,6 +151,7 @@ var GET_FINDUS_REQUEST = 'Services/Lists.svc/REST/Get_HMG_Locations';
///LiveChat
var GET_LIVECHAT_REQUEST = 'Services/Patients.svc/REST/GetPatientICProjects';
var GET_LIVECHAT_REQUEST_ID = 'Services/Patients.svc/REST/Patient_ICChatRequest_Insert';
///babyInformation
var GET_BABYINFORMATION_REQUEST = 'Services/Community.svc/REST/GetBabyByUserID';

@ -14,6 +14,8 @@ abstract class ContactUsRepo {
Future<Either<Failure, GenericApiModel<List<GetPatientICProjectsModel>>>> getLiveChatProjectsList();
Future<Either<Failure, GenericApiModel<String>>> getChatRequestID({required String name, required String mobileNo, required String workGroup});
Future<Either<Failure, GenericApiModel<dynamic>>> insertCOCItem({required RequestInsertCOCItem requestInsertCOCItem, PatientAppointmentHistoryResponseModel? patientSelectedAppointment});
}
@ -97,6 +99,45 @@ class ContactUsRepoImp implements ContactUsRepo {
}
}
@override
Future<Either<Failure, GenericApiModel<String>>> getChatRequestID({required String name, required String mobileNo, required String workGroup}) async {
Map<String, dynamic> body = {};
body['Name'] = name;
body['MobileNo'] = mobileNo;
body['WorkGroup'] = workGroup;
try {
GenericApiModel<String>? apiResponse;
Failure? failure;
await apiClient.post(
GET_LIVECHAT_REQUEST_ID,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final requestId = response['RequestId'] as String;
apiResponse = GenericApiModel<String>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: requestId,
);
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<dynamic>>> insertCOCItem({required RequestInsertCOCItem requestInsertCOCItem, PatientAppointmentHistoryResponseModel? patientSelectedAppointment}) async {
final Map<String, dynamic> body = requestInsertCOCItem.toJson();

@ -29,6 +29,8 @@ class ContactUsViewModel extends ChangeNotifier {
int selectedLiveChatProjectIndex = -1;
String? chatRequestID;
List<String> feedbackAttachmentList = [];
PatientAppointmentHistoryResponseModel? patientFeedbackSelectedAppointment;
@ -153,6 +155,32 @@ class ContactUsViewModel extends ChangeNotifier {
);
}
Future<void> getChatRequestID({required String name, required String mobileNo, required String workGroup, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await contactUsRepo.getChatRequestID(name: name, mobileNo: mobileNo, workGroup: workGroup);
result.fold(
(failure) async {
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Unknown error');
}
} else if (apiResponse.messageStatus == 1) {
chatRequestID = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> insertCOCItem({required String subject, required String message, Function(dynamic)? onSuccess, Function(String)? onError}) async {
RequestInsertCOCItem requestInsertCOCItem = RequestInsertCOCItem();
requestInsertCOCItem.attachment = feedbackAttachmentList.isNotEmpty ? feedbackAttachmentList.first : "";

@ -940,7 +940,16 @@ class HmgServicesRepoImp implements HmgServicesRepo {
for (var vitalSignJson in vitalSignsList) {
if (vitalSignJson is Map<String, dynamic>) {
vitalSignList.add(VitalSignResModel.fromJson(vitalSignJson));
final vitalSign = VitalSignResModel.fromJson(vitalSignJson);
// Only add records where BOTH height AND weight are greater than 0
final hasValidWeight = _isValidValue(vitalSign.weightKg);
final hasValidHeight = _isValidValue(vitalSign.heightCm);
// Only add if both height and weight are valid (> 0)
if (hasValidWeight && hasValidHeight) {
vitalSignList.add(vitalSign);
}
}
}
}
@ -967,5 +976,22 @@ class HmgServicesRepoImp implements HmgServicesRepo {
}
}
/// Helper method to check if a value is valid (greater than 0)
bool _isValidValue(dynamic value) {
if (value == null) return false;
if (value is num) {
return value > 0;
}
if (value is String) {
if (value.trim().isEmpty) return false;
final parsed = double.tryParse(value);
return parsed != null && parsed > 0;
}
return false;
}
}

@ -129,9 +129,13 @@ class LiveChatPage extends StatelessWidget {
).paddingSymmetrical(16.h, 16.h),
).onPress(() {
contactUsVM.setSelectedLiveChatProjectIndex(index);
chatURL =
"https://chat.hmg.com/Index.aspx?Name=${appState.getAuthenticatedUser()!.firstName}&PatientID=${appState.getAuthenticatedUser()!.patientId}&MobileNo=${appState.getAuthenticatedUser()!.mobileNumber}&Language=${appState.isArabic() ? 'ar' : 'en'}&WorkGroup=${contactUsVM.liveChatProjectsList[index].value}";
debugPrint("Chat URL: $chatURL");
_getChatRequestID(
context,
contactUsVM,
name: appState.getAuthenticatedUser()!.firstName ?? '',
mobileNo: appState.getAuthenticatedUser()!.mobileNumber ?? '',
workGroup: contactUsVM.liveChatProjectsList[index].value ?? '',
);
}),
).paddingSymmetrical(24.h, 0.h),
),
@ -154,8 +158,14 @@ class LiveChatPage extends StatelessWidget {
child: CustomButton(
text: LocaleKeys.liveChat.tr(context: context),
onPressed: () async {
Uri uri = Uri.parse(chatURL);
launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: "");
if (contactUsVM.chatRequestID != null) {
chatURL = "https://chat.hmg.com/Index.aspx?RequestedId=${contactUsVM.chatRequestID}";
debugPrint("Chat URL: $chatURL");
Uri uri = Uri.parse(chatURL);
launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: "");
} else {
debugPrint("Chat Request ID is null");
}
},
backgroundColor: contactUsVM.selectedLiveChatProjectIndex == -1 ? AppColors.greyColor : AppColors.primaryRedColor,
borderColor: contactUsVM.selectedLiveChatProjectIndex == -1 ? AppColors.greyColor : AppColors.primaryRedColor,
@ -172,4 +182,20 @@ class LiveChatPage extends StatelessWidget {
}),
);
}
void _getChatRequestID(BuildContext context, ContactUsViewModel contactUsVM, {required String name, required String mobileNo, required String workGroup}) {
contactUsVM.getChatRequestID(
name: name,
mobileNo: mobileNo,
workGroup: workGroup,
onSuccess: (response) {
debugPrint("Chat Request ID received: ${contactUsVM.chatRequestID}");
chatURL = "https://chat.hmg.com/Index.aspx?RequestedId=${contactUsVM.chatRequestID}";
debugPrint("Chat URL: $chatURL");
},
onError: (error) {
debugPrint("Error getting chat request ID: $error");
},
);
}
}

@ -0,0 +1,284 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/notifications/models/resp_models/notification_response_model.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:intl/intl.dart';
import 'package:share_plus/share_plus.dart';
class NotificationDetailsPage extends StatelessWidget {
final NotificationResponseModel notification;
const NotificationDetailsPage({
super.key,
required this.notification,
});
@override
Widget build(BuildContext context) {
// Debug logging
print('=== Notification Details ===');
print('Message: ${notification.message}');
print('MessageType: ${notification.messageType}');
print('MessageTypeData: ${notification.messageTypeData}');
print('VideoURL: ${notification.videoURL}');
print('========================');
return CollapsingListView(
title: "Notification Details".needTranslation,
trailing: IconButton(
icon: Icon(
Icons.share_outlined,
size: 24.h,
color: AppColors.textColor,
),
onPressed: () {
_shareNotification();
},
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Notification content card
_buildNotificationCard(context),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0.h),
),
);
}
Widget _buildNotificationCard(BuildContext context) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Date and Time row
Row(
children: [
// Time chip with clock icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.access_time, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatTime(notification.isSentOn).toText10(
weight: FontWeight.w500,
color: AppColors.textColor,
),
],
),
),
SizedBox(width: 8.w),
// Date chip with calendar icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatDate(notification.isSentOn).toText10(
weight: FontWeight.w500,
color: AppColors.textColor,
),
],
),
),
],
),
SizedBox(height: 16.h),
// Notification message
if (notification.message != null && notification.message!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
'Message'.needTranslation.toText14(
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
notification.message!.toText16(
weight: FontWeight.w400,
color: AppColors.textColor,
maxlines: 100,
),
SizedBox(height: 16.h),
],
),
// Notification image (if MessageType is "image")
if (notification.messageType != null &&
notification.messageType!.toLowerCase() == "image")
Builder(
builder: (context) {
// Try to get image URL from videoURL or messageTypeData
String? imageUrl;
if (notification.videoURL != null && notification.videoURL!.isNotEmpty) {
imageUrl = notification.videoURL;
print('Image URL from videoURL: $imageUrl');
} else if (notification.messageTypeData != null && notification.messageTypeData!.isNotEmpty) {
imageUrl = notification.messageTypeData;
print('Image URL from messageTypeData: $imageUrl');
}
if (imageUrl == null || imageUrl.isEmpty) {
print('No image URL found. videoURL: ${notification.videoURL}, messageTypeData: ${notification.messageTypeData}');
return SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
'Attached Image'.needTranslation.toText14(
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
ClipRRect(
borderRadius: BorderRadius.circular(12.h),
child: Image.network(
imageUrl,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('Error loading image: $error');
print('Image URL: $imageUrl');
return Container(
height: 200.h,
decoration: BoxDecoration(
color: AppColors.greyColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12.h),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.broken_image_outlined,
size: 48.h,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
'Failed to load image'.needTranslation.toText12(
color: AppColors.greyTextColor,
),
SizedBox(height: 4.h),
Text(
imageUrl!,
style: TextStyle(fontSize: 8, color: AppColors.greyTextColor),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
print('Image loaded successfully');
return child;
}
return Container(
height: 200.h,
decoration: BoxDecoration(
color: AppColors.greyColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12.h),
),
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
),
),
SizedBox(height: 16.h),
],
);
},
),
// Additional notification info
if (notification.notificationType != null && notification.notificationType!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
'Type'.needTranslation.toText14(
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
notification.notificationType!.toText16(
weight: FontWeight.w400,
color: AppColors.textColor,
),
],
),
],
),
);
}
void _shareNotification() async {
final String shareText = '''
${notification.message ?? 'Notification'}
Time: ${_formatTime(notification.isSentOn)}
Date: ${_formatDate(notification.isSentOn)}
${notification.notificationType != null ? '\nType: ${notification.notificationType}' : ''}
'''.trim();
await Share.share(shareText);
}
String _formatTime(String? dateTimeString) {
if (dateTimeString == null || dateTimeString.isEmpty) return '--';
try {
final dateTime = DateUtil.convertStringToDate(dateTimeString);
return DateFormat('hh:mm a').format(dateTime);
} catch (e) {
return '--';
}
}
String _formatDate(String? dateTimeString) {
if (dateTimeString == null || dateTimeString.isEmpty) return '--';
try {
final dateTime = DateUtil.convertStringToDate(dateTimeString);
return DateFormat('dd MMM yyyy').format(dateTime);
} catch (e) {
return '--';
}
}
}

@ -1,15 +1,19 @@
import 'package:flutter/material.dart';
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/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/int_extensions.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart';
import 'package:hmg_patient_app_new/presentation/notifications/notification_details_page.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
class NotificationsListPage extends StatelessWidget {
const NotificationsListPage({super.key});
@ -46,24 +50,134 @@ class NotificationsListPage extends StatelessWidget {
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// "Notification Title".toText14(),
// SizedBox(height: 8.h),
Row(
children: [
Expanded(child: notificationsVM.notificationsList[index].message!.toText16(isBold: notificationsVM.notificationsList[index].isRead ?? false)),
],
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NotificationDetailsPage(
notification: notificationsVM.notificationsList[index],
),
),
SizedBox(height: 12.h),
DateUtil.formatDateToDate(DateUtil.convertStringToDate(notificationsVM.notificationsList[index].isSentOn!), false).toText14(weight: FontWeight.w500),
1.divider,
],
);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// Message row with red dot for unread
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: notificationsVM.notificationsList[index].message!.toText16(
isBold: (notificationsVM.notificationsList[index].isRead == false),
weight: (notificationsVM.notificationsList[index].isRead == false)
? FontWeight.w600
: FontWeight.w400,
),
),
SizedBox(width: 8.w),
// Red dot for unread notifications ONLY
if (notificationsVM.notificationsList[index].isRead == false)
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
],
),
SizedBox(height: 12.h),
// First row: Time and Date chips with arrow
Row(
children: [
// Time chip with clock icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.access_time, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatTime(notificationsVM.notificationsList[index].isSentOn).toText10(
weight: FontWeight.w500,
color: AppColors.textColor,
),
],
),
),
SizedBox(width: 8.w),
// Date chip with calendar icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatDate(notificationsVM.notificationsList[index].isSentOn).toText10(
weight: FontWeight.w500,
color: AppColors.textColor,
),
],
),
),
Spacer(),
// Arrow icon
Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
width: 16.w,
height: 16.h,
iconColor: AppColors.greyTextColor,
),
],
),
// Second row: Contains Image chip (if MessageType is "image")
if (notificationsVM.notificationsList[index].messageType != null &&
notificationsVM.notificationsList[index].messageType!.toLowerCase() == "image")
Padding(
padding: EdgeInsets.only(top: 8.h),
child: Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.image_outlined, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
'Contains Image'.toText10(
weight: FontWeight.w500,
color: AppColors.textColor,
),
],
),
),
],
),
),
SizedBox(height: 16.h),
1.divider,
],
),
),
),
),
@ -75,4 +189,24 @@ class NotificationsListPage extends StatelessWidget {
),
);
}
String _formatTime(String? dateTimeString) {
if (dateTimeString == null || dateTimeString.isEmpty) return '--';
try {
final dateTime = DateUtil.convertStringToDate(dateTimeString);
return DateFormat('hh:mm a').format(dateTime);
} catch (e) {
return '--';
}
}
String _formatDate(String? dateTimeString) {
if (dateTimeString == null || dateTimeString.isEmpty) return '--';
try {
final dateTime = DateUtil.convertStringToDate(dateTimeString);
return DateFormat('dd MMM yyyy').format(dateTime);
} catch (e) {
return '--';
}
}
}

@ -85,7 +85,8 @@ class AppRoutes {
static const String addHealthTrackerEntryPage = '/addHealthTrackerEntryPage';
static const String healthTrackerDetailPage = '/healthTrackerDetailPage';
static Map<String, WidgetBuilder> get routes => {
static Map<String, WidgetBuilder> get routes =>
{
initialRoute: (context) => SplashPage(),
loginScreen: (context) => LoginScreen(),
landingScreen: (context) => LandingNavigation(),
@ -116,13 +117,19 @@ class AppRoutes {
healthTrackersPage: (context) => HealthTrackersPage(),
vitalSign: (context) => VitalSignPage(),
addHealthTrackerEntryPage: (context) {
final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?;
final args = ModalRoute
.of(context)
?.settings
.arguments as HealthTrackerTypeEnum?;
return AddHealthTrackerEntryPage(
trackerType: args ?? HealthTrackerTypeEnum.bloodSugar,
);
},
healthTrackerDetailPage: (context) {
final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?;
final args = ModalRoute
.of(context)
?.settings
.arguments as HealthTrackerTypeEnum?;
return HealthTrackerDetailPage(
trackerType: args ?? HealthTrackerTypeEnum.bloodSugar,
);

Loading…
Cancel
Save