From e8319a0d3f1b0988285c33365c04bad3ab8673aa Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 8 Oct 2025 12:26:23 +0300 Subject: [PATCH 01/12] active medication --- .../active_medication_page.dart | 682 ++++++++++++++++++ lib/presentation/home/landing_page.dart | 11 +- 2 files changed, 692 insertions(+), 1 deletion(-) create mode 100644 lib/presentation/active_medication/active_medication_page.dart diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart new file mode 100644 index 0000000..21e1a8b --- /dev/null +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -0,0 +1,682 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; + +import '../../core/app_assets.dart'; +import '../../core/utils/utils.dart'; +import '../../generated/locale_keys.g.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import 'package:intl/intl.dart'; + +import '../../widgets/buttons/custom_button.dart'; +import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting + + + +class ActiveMedicationPage extends StatefulWidget { + @override + State createState() => _ActiveMedicationPageState(); +} + +class _ActiveMedicationPageState extends State { + + late DateTime currentDate; + late DateTime selectedDate; + +// Info for each day (customizable) + final Map> dayInfo = { + 0: {"text": "Medications", "icon": Icons.medication_outlined, "description": "Affected"}, + 1: {"text": "Doctor Appointment", "icon": Icons.local_hospital_outlined, "description": "Twice"}, + 2: {"text": "Rest Day", "icon": Icons.self_improvement_outlined, "description": "Daily"}, + 3: {"text": "Gym Session", "icon": Icons.fitness_center_outlined, "description": "Affected"}, + 4: {"text": "Meeting", "icon": Icons.meeting_room_outlined, "description": "Twice"}, + 5: {"text": "Shopping", "icon": Icons.shopping_bag_outlined, "description": "Daily"}, + 6: {"text": "Family Time", "icon": Icons.family_restroom_outlined, "description": "Affected"}, + }; + + @override + void initState() { + super.initState(); + currentDate = DateTime.now(); + selectedDate = currentDate; + } + +// Generate today + next 6 days + List getUpcomingDays() { + return List.generate(7, (index) => currentDate.add(Duration(days: index))); + } + + @override + Widget build(BuildContext context) { + + List days = getUpcomingDays(); + int dayIndex = selectedDate.difference(currentDate).inDays; + + String dateText = + "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; + String infoMed = dayInfo[dayIndex]?["text"] ?? "No Info"; + IconData infoImg= dayInfo[dayIndex]?["icon"] ?? Icons.info_outline; + String medDetails = dayInfo[dayIndex]?["description"] ?? "No Info"; + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () { + Navigator.of(context).pop(); + }, + onLanguageChanged: (lang) {}, + hideLogoAndLang: true, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 65, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: days.length, + itemBuilder: (context, index) { + DateTime day = days[index]; + String label = DateFormat('E').format(day); // Mon, Tue + return Padding( + padding: const EdgeInsets.only(right: 12), + child: buildDayCard(label, day), + ); + }, + ), + ), + + const SizedBox(height: 20), + +// Show full date text + Text( + dateText, + style: TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500), + ), + const Text( + "Medications", + style: TextStyle( + color: AppColors.primaryRedBorderColor,fontSize: 12, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 16), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, + hasShadow: true,), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(1), + child: Container( + width: 59, + height: 59, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.spacerLineColor,// Border color + width: 1.0, ), + borderRadius: BorderRadius.circular(30),// Border width + ), + child: + Icon(infoImg, size: 26), + // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + ), + ), + const SizedBox(width: 12), + Text( + infoMed, + style: TextStyle( + fontSize: 16, + height: 1.2, + fontWeight: FontWeight.w700, + color: Colors.black87), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "Route: $medDetails", + ), + AppCustomChipWidget( + labelText: "Frequency: $medDetails", + ), + AppCustomChipWidget( + labelText: "Daily Does $medDetails", + ), + AppCustomChipWidget( + labelText: "Duration: $medDetails ", + ), + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: Colors.grey,), + const SizedBox(width: 8), + Expanded( + child: Text( + "Remark: some remarks about the prescription will be here", + style: TextStyle( + fontSize: 10, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.visible, + ), + ) + ], + ), + ], + ).paddingAll(16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + // Reminder Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10),// Border width + ), + child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Set Reminder", + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, + color: AppColors.textColor), + ), + const Text( + "Notify me before the consumption time", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, + color: AppColors.textColorLight), + ), + ], + ), + ], + ), + // Switch( + // value: isActiveReminder, + // onChanged: (_) {}, + // activeColor: Colors.green, + // ), + ], + ).paddingOnly(left:16, right: 16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + +// Buttons + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.checkAvailability.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomButton( + text: LocaleKeys.readInstructions.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + ), + ), + ], + ).paddingAll(16), + ], + ), + ) +// Expanded( +// child: ListView( +// children: [ +// Container( +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, +// hasShadow: true,), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Row( +// children: [ +// ClipRRect( +// borderRadius: BorderRadius.circular(1), +// child: Container( +// width: 59, +// height: 59, +// decoration: BoxDecoration( +// border: Border.all( +// color: AppColors.spacerLineColor,// Border color +// width: 1.0, ), +// borderRadius: BorderRadius.circular(30),// Border width +// ), +// child: +// Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) +// ), +// ), +// const SizedBox(width: 12), +// const Expanded( +// child: Text( +// "Diclofenac Diethylamine 1% Topical Gel", +// style: TextStyle( +// fontSize: 16, +// height: 1.2, +// fontWeight: FontWeight.w700, +// color: Colors.black87), +// ), +// ), +// ], +// ), +// const SizedBox(height: 12), +// Wrap( +// direction: Axis.horizontal, +// spacing: 4.h, +// runSpacing: 4.h, +// children: [ +// AppCustomChipWidget( +// labelText: "Route: Affected Area ", +// ), +// AppCustomChipWidget( +// labelText: "Route: Affected Area ", +// ), +// AppCustomChipWidget( +// labelText: "Daily Does 2", +// ), +// AppCustomChipWidget( +// labelText: "Route: Affected Area ", +// ), +// ], +// ), +// const SizedBox(height: 12), +// Row( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Icon(Icons.info_outline, color: Colors.grey,), +// const SizedBox(width: 8), +// Expanded( +// child: Text( +// "Remark: some remarks about the prescription will be here", +// style: TextStyle( +// fontSize: 10, +// color: AppColors.greyTextColor, +// fontWeight: FontWeight.w500, +// ), +// overflow: TextOverflow.visible, +// ), +// ) +// ], +// ), +// ], +// ).paddingAll(16), +// const Divider( +// indent: 0, +// endIndent: 0, +// thickness: 1, +// color: AppColors.greyColor, +// ), +// // Reminder Row +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Row( +// children: [ +// Container( +// width: 40, +// height: 40, +// decoration: BoxDecoration( +// color: AppColors.greyColor, +// borderRadius: BorderRadius.circular(10),// Border width +// ), +// child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), +// const SizedBox(width: 8), +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// const Text( +// "Set Reminder", +// style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, +// color: AppColors.textColor), +// ), +// const Text( +// "Notify me before the consumption time", +// style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, +// color: AppColors.textColorLight), +// ), +// ], +// ), +// ], +// ), +// // Switch( +// // value: isActiveReminder, +// // onChanged: (_) {}, +// // activeColor: Colors.green, +// // ), +// ], +// ).paddingOnly(left:16, right: 16), +// const Divider( +// indent: 0, +// endIndent: 0, +// thickness: 1, +// color: AppColors.greyColor, +// ), +// +// // Buttons +// Row( +// children: [ +// Expanded( +// child: CustomButton( +// text: LocaleKeys.checkAvailability.tr(), +// fontSize: 14, +// onPressed: () async { +// }, +// backgroundColor: AppColors.secondaryLightRedColor, +// borderColor: AppColors.secondaryLightRedColor, +// textColor: AppColors.errorColor, +// ), +// ), +// const SizedBox(width: 12), +// Expanded( +// child: CustomButton( +// text: LocaleKeys.readInstructions.tr(), +// fontSize: 14, +// onPressed: () async { +// }, +// backgroundColor: AppColors.primaryRedColor, +// borderColor: AppColors.primaryRedColor, +// textColor: AppColors.whiteColor, +// ), +// ), +// ], +// ).paddingAll(16), +// ], +// ), +// ) +// // MedicationCard(), +// // SizedBox(height: 16), +// // MedicationCard(isActiveReminder: true), +// ], +// ), +// ), + ] + ), + + ), + ); + } + + Widget buildDayCard(String label, DateTime date) { + bool isSelected = selectedDate.day == date.day && + selectedDate.month == date.month && + selectedDate.year == date.year; + + return GestureDetector( + onTap: () { + setState(() { + selectedDate = date; + }); + }, + child: Container( + width: 57, + height: 65, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: isSelected ? AppColors.secondaryLightRedBorderColor: AppColors.transparent, + border: Border.all( + color: isSelected ? AppColors.primaryRedBorderColor : AppColors.spacerLineColor, + width: 1.0, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + date.day == currentDate.day ? "Today" : label, + style: TextStyle( + color: isSelected ? AppColors.primaryRedBorderColor : AppColors.greyTextColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 5), + Text( + date.day.toString(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isSelected ? AppColors.primaryRedBorderColor : AppColors.textColor, + ), + ), + ], + ), + ), + ), + ); + } + + String getSuffix(int day) { + if (day == 1 || day == 21 || day == 31) return "st"; + if (day == 2 || day == 22) return "nd"; + if (day == 3 || day == 23) return "rd"; + return "th"; + } +} + +class MedicationCard extends StatelessWidget { + final bool isActiveReminder; + const MedicationCard({super.key, this.isActiveReminder = false}); + + Color get primaryRed => const Color(0xFFE84B3A); + + @override + Widget build(BuildContext context) { + return + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, + hasShadow: true,), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(1), + child: Container( + width: 59, + height: 59, + decoration: BoxDecoration( + border: Border.all( + color: AppColors.spacerLineColor,// Border color + width: 1.0, ), + borderRadius: BorderRadius.circular(30),// Border width + ), + child: + Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + "Diclofenac Diethylamine 1% Topical Gel", + style: TextStyle( + fontSize: 16, + height: 1.2, + fontWeight: FontWeight.w700, + color: Colors.black87), + ), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget( + labelText: "Route: Affected Area ", + ), + AppCustomChipWidget( + labelText: "Route: Affected Area ", + ), + AppCustomChipWidget( + labelText: "Daily Does 2", + ), + AppCustomChipWidget( + labelText: "Route: Affected Area ", + ), + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: Colors.grey,), + const SizedBox(width: 8), + Expanded( + child: Text( + "Remark: some remarks about the prescription will be here", + style: TextStyle( + fontSize: 10, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.visible, + ), + ) + ], + ), + ], + ).paddingAll(16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + // Reminder Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10),// Border width + ), + child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Set Reminder", + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, + color: AppColors.textColor), + ), + const Text( + "Notify me before the consumption time", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, + color: AppColors.textColorLight), + ), + ], + ), + ], + ), + Switch( + value: isActiveReminder, + onChanged: (_) {}, + activeColor: Colors.green, + ), + ], + ).paddingOnly(left:16, right: 16), + const Divider( + indent: 0, + endIndent: 0, + thickness: 1, + color: AppColors.greyColor, + ), + +// Buttons + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.checkAvailability.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomButton( + text: LocaleKeys.readInstructions.tr(), + fontSize: 14, + onPressed: () async { + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + ), + ), + ], + ).paddingAll(16), + ], + ), + ); + } +} + diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index cb2f76f..2bb1b14 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -44,6 +44,8 @@ import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dar import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; +import '../active_medication/active_medication_page.dart'; + class LandingPage extends StatefulWidget { const LandingPage({super.key}); @@ -379,7 +381,14 @@ class _LandingPageState extends State { "Services".toText16(isBold: true), Row( children: [ - "View all services".toText12(color: AppColors.primaryRedColor), + "View all services".toText12(color: AppColors.primaryRedColor).onPress(() { + Navigator.of(context) + .push( + CustomPageRoute( + page: ActiveMedicationPage(), + ), + ); + }), SizedBox(width: 2.h), Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), ], From de8c7bc60523fcd041507130e9de461fd239280e Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Tue, 21 Oct 2025 10:51:45 +0300 Subject: [PATCH 02/12] active medication --- lib/core/api_consts.dart | 6 +- lib/core/utils/utils.dart | 2 +- .../active_prescriptions_repo.dart | 99 +++ .../active_prescriptions_view_model.dart | 57 ++ .../active_prescriptions_response_model.dart | 149 ++++ lib/main.dart | 4 + .../active_medication_page.dart | 714 ++++++++---------- lib/presentation/home/landing_page.dart | 6 - .../medical_file/medical_file_page.dart | 13 +- lib/services/dialog_service.dart | 16 + .../reminder_timer_dialog.dart | 155 ++++ 11 files changed, 794 insertions(+), 427 deletions(-) create mode 100644 lib/features/active_prescriptions/active_prescriptions_repo.dart create mode 100644 lib/features/active_prescriptions/active_prescriptions_view_model.dart create mode 100644 lib/features/active_prescriptions/models/active_prescriptions_response_model.dart create mode 100644 lib/widgets/medication_reminder/reminder_timer_dialog.dart diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 9591cfe..8d7ae29 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -419,7 +419,7 @@ var GET_WEIGHT_PRESSURE_RESULT_AVERAGE = 'Services/Patients.svc/REST/Patient_Get var GET_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_GetWeightMeasurementResult'; var ADD_WEIGHT_PRESSURE_RESULT = 'Services/Patients.svc/REST/Patient_AddWeightMeasurementResult'; -var ADD_ACTIVE_PRESCRIPTIONS_REPORT_BY_PATIENT_ID = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; +// var ADD_ACTIVE_PRESCRIPTIONS_REPORT_BY_PATIENT_ID = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; var GET_CALL_INFO_HOURS_RESULT = 'Services/Doctors.svc/REST/GetCallInfoHoursResult'; var GET_CALL_REQUEST_TYPE_LOV = 'Services/Doctors.svc/REST/GetCallRequestType_LOV'; @@ -727,7 +727,7 @@ const FAMILY_FILES= 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatu class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -838,7 +838,7 @@ class ApiConsts { static final String getAllSharedRecordsByStatus = 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatus'; static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; - + static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; // static values for Api static final double appVersionID = 18.7; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index c4a2db8..fe5dd94 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -643,7 +643,7 @@ class Utils { } /// Widget to build an SVG from network - static Widget buildImgWithNetwork({required String url, required Color iconColor, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover}) { + static Widget buildImgWithNetwork({required String url, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover}) { return Image.network( url, width: width, diff --git a/lib/features/active_prescriptions/active_prescriptions_repo.dart b/lib/features/active_prescriptions/active_prescriptions_repo.dart new file mode 100644 index 0000000..847d6e8 --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_repo.dart @@ -0,0 +1,99 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; + +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class ActivePrescriptionsRepo { + + Future>> getActivePrescriptionsDetails(); + +} + +class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + ActivePrescriptionsRepoImp({required this.loggerService, required this.apiClient}); + + @override + + Future>> getActivePrescriptionsDetails() async + { + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getActivePrescriptionsDetails, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + // final list = response['GetActivePrescriptionReportByPatientIDList']; + + // final prescriptionLists = list.map((item) => ActivePrescriptionsResponseModel.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + return ['List_ActiveGetPrescriptionReportByPatientID']; + //apiResponse; + } 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())); + } + } + + + + + // + // Future> getActiveMedications() { + // try { + // GenericApiModel? apiResponse; + // Failure? failure; + // return apiClient.post( + // ApiConsts.getActivePrescriptionsDetails, + // body: patientDeviceDataRequest, + // onFailure: (error, statusCode, {messageStatus, failureType}) { + // failure = failureType; + // }, + // onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + // try { + // apiResponse = GenericApiModel( + // messageStatus: messageStatus, + // statusCode: statusCode, + // errorMessage: errorMessage, + // data: response, + // ); + // } catch (e) { + // failure = DataParsingFailure(e.toString()); + // } + // }, + // ).then((_) { + // if (failure != null) return Left(failure!); + // if (apiResponse == null) return Left(ServerFailure("Unknown error")); + // return Right(apiResponse!); + // }); + // } catch (e) { + // return Future.value(Left(UnknownFailure(e.toString()))); + // } + // } +} \ No newline at end of file diff --git a/lib/features/active_prescriptions/active_prescriptions_view_model.dart b/lib/features/active_prescriptions/active_prescriptions_view_model.dart new file mode 100644 index 0000000..d995f96 --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -0,0 +1,57 @@ + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class ActivePrescriptionsViewModel extends ChangeNotifier { + bool isActivePrescriptionsDetailsLoading = false; + + late ActivePrescriptionsRepo activePrescriptionsRepo; + late ErrorHandlerService errorHandlerService; + + // Prescription Orders Lists + List activePrescriptionsDetailsList = []; + + initActivePrescriptionsViewModel() { + getActiveMedications(); + notifyListeners(); + } + + setPrescriptionsDetailsLoading() { + isActivePrescriptionsDetailsLoading = true; + // activePrescriptionsDetailsList.clear(); + notifyListeners(); + } + + Future getActiveMedications( {Function(dynamic)? onSuccess, Function(String)? onError}) + async { + final result = await activePrescriptionsRepo.getActivePrescriptionsDetails(); + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + activePrescriptionsDetailsList = apiResponse.data!; + isActivePrescriptionsDetailsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + print(activePrescriptionsDetailsList.length); + } + } + }, + ); + } + + + + + + + + + + +} diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart new file mode 100644 index 0000000..878e191 --- /dev/null +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -0,0 +1,149 @@ +import 'dart:convert'; + +class ActivePrescriptionsResponseModel { + dynamic address; + int? appointmentNo; + dynamic clinic; + dynamic companyName; + int? days; + dynamic doctorName; + int? doseDailyQuantity; + String? frequency; + int? frequencyNumber; + dynamic image; + dynamic imageExtension; + dynamic imageSrcUrl; + String? imageString; + dynamic imageThumbUrl; + dynamic isCovered; + String? itemDescription; + int? itemId; + String? orderDate; + int? patientId; + dynamic patientName; + dynamic phoneOffice1; + dynamic prescriptionQr; + int? prescriptionTimes; + dynamic productImage; + String? productImageBase64; + String? productImageString; + int? projectId; + dynamic projectName; + dynamic remarks; + String? route; + String? sku; + int? scaleOffset; + String? startDate; + + ActivePrescriptionsResponseModel({ + this.address, + this.appointmentNo, + this.clinic, + this.companyName, + this.days, + this.doctorName, + this.doseDailyQuantity, + this.frequency, + this.frequencyNumber, + this.image, + this.imageExtension, + this.imageSrcUrl, + this.imageString, + this.imageThumbUrl, + this.isCovered, + this.itemDescription, + this.itemId, + this.orderDate, + this.patientId, + this.patientName, + this.phoneOffice1, + this.prescriptionQr, + this.prescriptionTimes, + this.productImage, + this.productImageBase64, + this.productImageString, + this.projectId, + this.projectName, + this.remarks, + this.route, + this.sku, + this.scaleOffset, + this.startDate, + }); + + factory ActivePrescriptionsResponseModel.fromRawJson(String str) => ActivePrescriptionsResponseModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ActivePrescriptionsResponseModel.fromJson(Map json) => ActivePrescriptionsResponseModel( + address: json["Address"], + appointmentNo: json["AppointmentNo"], + clinic: json["Clinic"], + companyName: json["CompanyName"], + days: json["Days"], + doctorName: json["DoctorName"], + doseDailyQuantity: json["DoseDailyQuantity"], + frequency: json["Frequency"], + frequencyNumber: json["FrequencyNumber"], + image: json["Image"], + imageExtension: json["ImageExtension"], + imageSrcUrl: json["ImageSRCUrl"], + imageString: json["ImageString"], + imageThumbUrl: json["ImageThumbUrl"], + isCovered: json["IsCovered"], + itemDescription: json["ItemDescription"], + itemId: json["ItemID"], + orderDate: json["OrderDate"], + patientId: json["PatientID"], + patientName: json["PatientName"], + phoneOffice1: json["PhoneOffice1"], + prescriptionQr: json["PrescriptionQR"], + prescriptionTimes: json["PrescriptionTimes"], + productImage: json["ProductImage"], + productImageBase64: json["ProductImageBase64"], + productImageString: json["ProductImageString"], + projectId: json["ProjectID"], + projectName: json["ProjectName"], + remarks: json["Remarks"], + route: json["Route"], + sku: json["SKU"], + scaleOffset: json["ScaleOffset"], + startDate: json["StartDate"], + ); + + Map toJson() => { + "Address": address, + "AppointmentNo": appointmentNo, + "Clinic": clinic, + "CompanyName": companyName, + "Days": days, + "DoctorName": doctorName, + "DoseDailyQuantity": doseDailyQuantity, + "Frequency": frequency, + "FrequencyNumber": frequencyNumber, + "Image": image, + "ImageExtension": imageExtension, + "ImageSRCUrl": imageSrcUrl, + "ImageString": imageString, + "ImageThumbUrl": imageThumbUrl, + "IsCovered": isCovered, + "ItemDescription": itemDescription, + "ItemID": itemId, + "OrderDate": orderDate, + "PatientID": patientId, + "PatientName": patientName, + "PhoneOffice1": phoneOffice1, + "PrescriptionQR": prescriptionQr, + "PrescriptionTimes": prescriptionTimes, + "ProductImage": productImage, + "ProductImageBase64": productImageBase64, + "ProductImageString": productImageString, + "ProjectID": projectId, + "ProjectName": projectName, + "Remarks": remarks, + "Route": route, + "SKU": sku, + "ScaleOffset": scaleOffset, + "StartDate": startDate, + }; +} diff --git a/lib/main.dart b/lib/main.dart index 20507d0..cf038f9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.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/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; @@ -125,6 +126,9 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index 21e1a8b..6394002 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -1,64 +1,72 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; - -import '../../core/app_assets.dart'; -import '../../core/utils/utils.dart'; +// import 'package:sizer/sizer.dart'; +import '../../core/dependencies.dart'; +import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; +import '../../features/active_prescriptions/models/active_prescriptions_response_model.dart'; import '../../generated/locale_keys.g.dart'; +import '../../services/dialog_service.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import 'package:intl/intl.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +// import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import '../../widgets/buttons/custom_button.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting +import 'package:provider/provider.dart'; class ActiveMedicationPage extends StatefulWidget { + //inal List activePrescriptionsResponseModel; + + ActiveMedicationPage({super.key, }); + + + + @override State createState() => _ActiveMedicationPageState(); } class _ActiveMedicationPageState extends State { - late DateTime currentDate; late DateTime selectedDate; -// Info for each day (customizable) - final Map> dayInfo = { - 0: {"text": "Medications", "icon": Icons.medication_outlined, "description": "Affected"}, - 1: {"text": "Doctor Appointment", "icon": Icons.local_hospital_outlined, "description": "Twice"}, - 2: {"text": "Rest Day", "icon": Icons.self_improvement_outlined, "description": "Daily"}, - 3: {"text": "Gym Session", "icon": Icons.fitness_center_outlined, "description": "Affected"}, - 4: {"text": "Meeting", "icon": Icons.meeting_room_outlined, "description": "Twice"}, - 5: {"text": "Shopping", "icon": Icons.shopping_bag_outlined, "description": "Daily"}, - 6: {"text": "Family Time", "icon": Icons.family_restroom_outlined, "description": "Affected"}, - }; + + ActivePrescriptionsViewModel? activePreVM; @override void initState() { + activePreVM = Provider.of(context, listen: false); + activePreVM?.getActiveMedications(); + print(activePreVM?.activePrescriptionsDetailsList); super.initState(); currentDate = DateTime.now(); selectedDate = currentDate; } + // Generate today + next 6 days List getUpcomingDays() { return List.generate(7, (index) => currentDate.add(Duration(days: index))); } + // on/off toggle + bool isOn = true; + get index => null; @override Widget build(BuildContext context) { - + // activePreVM = Provider.of(context, listen: false); List days = getUpcomingDays(); int dayIndex = selectedDate.difference(currentDate).inDays; - - String dateText = - "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; - String infoMed = dayInfo[dayIndex]?["text"] ?? "No Info"; - IconData infoImg= dayInfo[dayIndex]?["icon"] ?? Icons.info_outline; - String medDetails = dayInfo[dayIndex]?["description"] ?? "No Info"; + String dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( @@ -74,10 +82,11 @@ class _ActiveMedicationPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: 65, + height: 65.h, child: ListView.builder( scrollDirection: Axis.horizontal, - itemCount: days.length, + itemCount: days.length, + // itemCount: widget.details.length, itemBuilder: (context, index) { DateTime day = days[index]; String label = DateFormat('E').format(day); // Mon, Tue @@ -88,23 +97,22 @@ class _ActiveMedicationPageState extends State { }, ), ), - - const SizedBox(height: 20), + SizedBox(height: 20.h), // Show full date text Text( dateText, style: TextStyle( color: AppColors.textColor, - fontSize: 16, + fontSize: 16.fSize, fontWeight: FontWeight.w500), ), - const Text( - "Medications", + Text( + "Medications".needTranslation, style: TextStyle( - color: AppColors.primaryRedBorderColor,fontSize: 12, fontWeight: FontWeight.w500), + color: AppColors.primaryRedBorderColor,fontSize: 12.fSize, fontWeight: FontWeight.w500), ), - const SizedBox(height: 16), + SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, hasShadow: true,), @@ -119,61 +127,64 @@ class _ActiveMedicationPageState extends State { ClipRRect( borderRadius: BorderRadius.circular(1), child: Container( - width: 59, - height: 59, + width: 59.h, + height: 59.h, decoration: BoxDecoration( border: Border.all( color: AppColors.spacerLineColor,// Border color - width: 1.0, ), + width: 1.0.h, ), borderRadius: BorderRadius.circular(30),// Border width ), child: - Icon(infoImg, size: 26), - // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + Utils.buildImgWithNetwork(url: activePreVM!.activePrescriptionsDetailsList[index].productImageString.toString(),width: 26.h,) ), ), - const SizedBox(width: 12), + SizedBox(width: 12.h), Text( - infoMed, + activePreVM!.activePrescriptionsDetailsList[index].itemDescription.toString(), style: TextStyle( - fontSize: 16, - height: 1.2, + fontSize: 16.fSize, + height: 1.2.h, fontWeight: FontWeight.w700, color: Colors.black87), ), ], ), - const SizedBox(height: 12), + SizedBox(height: 12.h), + activePreVM!.activePrescriptionsDetailsList.length > 0 ? Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: "Route: $medDetails", + labelText: "Route: ${activePreVM?.activePrescriptionsDetailsList[index].route}", ), AppCustomChipWidget( - labelText: "Frequency: $medDetails", + labelText: "Frequency: ${activePreVM?.activePrescriptionsDetailsList[index].frequency}".needTranslation, ), AppCustomChipWidget( - labelText: "Daily Does $medDetails", + labelText: "Daily Does ${activePreVM?.activePrescriptionsDetailsList[index].doseDailyQuantity}".needTranslation, ), AppCustomChipWidget( - labelText: "Duration: $medDetails ", + labelText: "Duration: ${activePreVM?.activePrescriptionsDetailsList[index].days} ".needTranslation, ), ], + ): + Container( + child: Text("no data"), ), - const SizedBox(height: 12), + SizedBox(height: 12.h), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline, color: Colors.grey,), - const SizedBox(width: 8), + SizedBox(width: 8.h), Expanded( child: Text( - "Remark: some remarks about the prescription will be here", + "Remark: some remarks about the prescription will be here".needTranslation, style: TextStyle( - fontSize: 10, + fontSize: 10.fSize, color: AppColors.greyTextColor, fontWeight: FontWeight.w500, ), @@ -194,41 +205,96 @@ class _ActiveMedicationPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10),// Border width - ), - child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Set Reminder", - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, - color: AppColors.textColor), - ), - const Text( - "Notify me before the consumption time", - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, - color: AppColors.textColorLight), - ), - ], + Container( + width: 40.h, + height: 40.h, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10),// Border width ), - ], + child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor) + // MedicalFileCard( + // label: "Vaccine Info".needTranslation, + // textColor: AppColors.blackColor, + // backgroundColor: AppColors.whiteColor, + // svgIcon: AppAssets..bell, + // isLargeText: true, + // iconSize: 36.h, + // ) + ), + SizedBox(width: 8.h), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Set Reminder", + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14.fSize, + color: AppColors.textColor), + ), + Text( + "Notify me before the consumption time", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12.fSize, + color: AppColors.textColorLight), + ), + ], + ).onPress(() { + DialogService dialogService = getIt.get(); + dialogService.showReminderBottomSheetWithoutHWithChild( + label: "Set the timer for reminder".needTranslation, + message: "", + child: ReminderTimerDialog(), + onOkPressed: () {}, + ); + }), + ), + GestureDetector( + onTap: () { + setState(() { + isOn = !isOn; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: isOn ? AppColors.lightGreenColor: AppColors.greyColor, + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: isOn ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isOn ? AppColors.textGreenColor : AppColors.greyTextColor, + ), + ), ), + ), + ), + ), + SizedBox(width: 2.h), // Switch( - // value: isActiveReminder, - // onChanged: (_) {}, - // activeColor: Colors.green, + // value: isOn, + // onChanged: (value){ + // setState(() { + // isOn = value; + // }); + // }, + // activeColor: AppColors.lightGreenColor, + // activeTrackColor: AppColors.lightGreenColor, + // activeThumbColor: AppColors.textGreenColor, + // inactiveThumbColor: AppColors.greyTextColor, + // inactiveTrackColor: AppColors.greyColor, // ), ], - ).paddingOnly(left:16, right: 16), + ).paddingAll(16), const Divider( indent: 0, endIndent: 0, @@ -242,7 +308,7 @@ class _ActiveMedicationPageState extends State { Expanded( child: CustomButton( text: LocaleKeys.checkAvailability.tr(), - fontSize: 14, + fontSize: 14.fSize, onPressed: () async { }, backgroundColor: AppColors.secondaryLightRedColor, @@ -250,11 +316,11 @@ class _ActiveMedicationPageState extends State { textColor: AppColors.errorColor, ), ), - const SizedBox(width: 12), + SizedBox(width: 12.h), Expanded( child: CustomButton( text: LocaleKeys.readInstructions.tr(), - fontSize: 14, + fontSize: 14.fSize, onPressed: () async { }, backgroundColor: AppColors.primaryRedColor, @@ -267,186 +333,15 @@ class _ActiveMedicationPageState extends State { ], ), ) -// Expanded( -// child: ListView( -// children: [ -// Container( -// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, -// hasShadow: true,), -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Row( -// children: [ -// ClipRRect( -// borderRadius: BorderRadius.circular(1), -// child: Container( -// width: 59, -// height: 59, -// decoration: BoxDecoration( -// border: Border.all( -// color: AppColors.spacerLineColor,// Border color -// width: 1.0, ), -// borderRadius: BorderRadius.circular(30),// Border width -// ), -// child: -// Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) -// ), -// ), -// const SizedBox(width: 12), -// const Expanded( -// child: Text( -// "Diclofenac Diethylamine 1% Topical Gel", -// style: TextStyle( -// fontSize: 16, -// height: 1.2, -// fontWeight: FontWeight.w700, -// color: Colors.black87), -// ), -// ), -// ], -// ), -// const SizedBox(height: 12), -// Wrap( -// direction: Axis.horizontal, -// spacing: 4.h, -// runSpacing: 4.h, -// children: [ -// AppCustomChipWidget( -// labelText: "Route: Affected Area ", -// ), -// AppCustomChipWidget( -// labelText: "Route: Affected Area ", -// ), -// AppCustomChipWidget( -// labelText: "Daily Does 2", -// ), -// AppCustomChipWidget( -// labelText: "Route: Affected Area ", -// ), -// ], -// ), -// const SizedBox(height: 12), -// Row( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Icon(Icons.info_outline, color: Colors.grey,), -// const SizedBox(width: 8), -// Expanded( -// child: Text( -// "Remark: some remarks about the prescription will be here", -// style: TextStyle( -// fontSize: 10, -// color: AppColors.greyTextColor, -// fontWeight: FontWeight.w500, -// ), -// overflow: TextOverflow.visible, -// ), -// ) -// ], -// ), -// ], -// ).paddingAll(16), -// const Divider( -// indent: 0, -// endIndent: 0, -// thickness: 1, -// color: AppColors.greyColor, -// ), -// // Reminder Row -// Row( -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// Row( -// children: [ -// Container( -// width: 40, -// height: 40, -// decoration: BoxDecoration( -// color: AppColors.greyColor, -// borderRadius: BorderRadius.circular(10),// Border width -// ), -// child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), -// const SizedBox(width: 8), -// Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// const Text( -// "Set Reminder", -// style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, -// color: AppColors.textColor), -// ), -// const Text( -// "Notify me before the consumption time", -// style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, -// color: AppColors.textColorLight), -// ), -// ], -// ), -// ], -// ), -// // Switch( -// // value: isActiveReminder, -// // onChanged: (_) {}, -// // activeColor: Colors.green, -// // ), -// ], -// ).paddingOnly(left:16, right: 16), -// const Divider( -// indent: 0, -// endIndent: 0, -// thickness: 1, -// color: AppColors.greyColor, -// ), -// -// // Buttons -// Row( -// children: [ -// Expanded( -// child: CustomButton( -// text: LocaleKeys.checkAvailability.tr(), -// fontSize: 14, -// onPressed: () async { -// }, -// backgroundColor: AppColors.secondaryLightRedColor, -// borderColor: AppColors.secondaryLightRedColor, -// textColor: AppColors.errorColor, -// ), -// ), -// const SizedBox(width: 12), -// Expanded( -// child: CustomButton( -// text: LocaleKeys.readInstructions.tr(), -// fontSize: 14, -// onPressed: () async { -// }, -// backgroundColor: AppColors.primaryRedColor, -// borderColor: AppColors.primaryRedColor, -// textColor: AppColors.whiteColor, -// ), -// ), -// ], -// ).paddingAll(16), -// ], -// ), -// ) -// // MedicationCard(), -// // SizedBox(height: 16), -// // MedicationCard(isActiveReminder: true), -// ], -// ), -// ), ] ), - ), + + ), ); } - Widget buildDayCard(String label, DateTime date) { + Widget buildDayCard(String label, DateTime date,) { bool isSelected = selectedDate.day == date.day && selectedDate.month == date.month && selectedDate.year == date.year; @@ -458,14 +353,14 @@ class _ActiveMedicationPageState extends State { }); }, child: Container( - width: 57, - height: 65, + width: 57.h, + height: 65.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: isSelected ? AppColors.secondaryLightRedBorderColor: AppColors.transparent, border: Border.all( color: isSelected ? AppColors.primaryRedBorderColor : AppColors.spacerLineColor, - width: 1.0, + width: 1.0.h, ), ), child: Padding( @@ -474,18 +369,18 @@ class _ActiveMedicationPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - date.day == currentDate.day ? "Today" : label, + date.day == currentDate.day ? "Today".needTranslation : label, style: TextStyle( color: isSelected ? AppColors.primaryRedBorderColor : AppColors.greyTextColor, - fontSize: 12, + fontSize: 12.fSize, fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 5), + SizedBox(height: 5.h), Text( date.day.toString(), style: TextStyle( - fontSize: 16, + fontSize: 16.fSize, fontWeight: FontWeight.bold, color: isSelected ? AppColors.primaryRedBorderColor : AppColors.textColor, ), @@ -503,180 +398,167 @@ class _ActiveMedicationPageState extends State { if (day == 3 || day == 23) return "rd"; return "th"; } + + // Widget manageReminder(){ + // NavigationService navigationService = getIt(); + // return Container( + // width: 59, + // height: 59, + // decoration: BoxDecoration( + // border: Border.all( + // color: AppColors.spacerLineColor,// Border color + // width: 1.0, ), + // borderRadius: BorderRadius.circular(30),// Border width + // ), + // child: + // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) + // ); + // } +} + +class ReminderTimerDialog extends StatefulWidget { + // final Function()? onSetReminderPress; + // final String message; + // + // const ReminderTimerDialog(this.onSetReminderPress, this.message, {super.key}); + const ReminderTimerDialog({super.key}); + + @override + State createState() => _ReminderTimerDialogState(); } -class MedicationCard extends StatelessWidget { - final bool isActiveReminder; - const MedicationCard({super.key, this.isActiveReminder = false}); +class _ReminderTimerDialogState extends State { + final List options = ["Morning", "Afternoon", "Evening", "Midnight"]; + final List selectedTimes = ["Morning"]; // Default selection - Color get primaryRed => const Color(0xFFE84B3A); @override Widget build(BuildContext context) { - return - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, - hasShadow: true,), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return // + Column( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(1), - child: Container( - width: 59, - height: 59, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.spacerLineColor,// Border color - width: 1.0, ), - borderRadius: BorderRadius.circular(30),// Border width - ), - child: - Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) - ), - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - "Diclofenac Diethylamine 1% Topical Gel", + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, + hasShadow: true,), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + // Checkboxes list + children: options.map((time) => buildCircleCheckbox(time)).toList(), + ).paddingAll(16), + ), + SizedBox(height: 25.h), + // Buttons Row + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, color: AppColors.errorColor), + label: Text( + LocaleKeys.cancel.tr(), style: TextStyle( - fontSize: 16, - height: 1.2, - fontWeight: FontWeight.w700, - color: Colors.black87), + color: AppColors.errorColor, + fontWeight: FontWeight.w500, + fontSize: 14.fSize + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.secondaryLightRedColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), ), - ], - ), - const SizedBox(height: 12), - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - labelText: "Route: Affected Area ", - ), - AppCustomChipWidget( - labelText: "Route: Affected Area ", - ), - AppCustomChipWidget( - labelText: "Daily Does 2", - ), - AppCustomChipWidget( - labelText: "Route: Affected Area ", - ), - ], - ), - const SizedBox(height: 12), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.info_outline, color: Colors.grey,), - const SizedBox(width: 8), - Expanded( - child: Text( - "Remark: some remarks about the prescription will be here", + ), + SizedBox(width: 12.h), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(context, selectedTimes); + }, + icon: const Icon(Icons.notifications_rounded), + label: Text( + LocaleKeys.setReminder.tr(), style: TextStyle( - fontSize: 10, - color: AppColors.greyTextColor, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w500, + fontSize: 14.fSize ), - overflow: TextOverflow.visible, ), - ) - ], - ), - ], - ).paddingAll(16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, - ), - // Reminder Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10),// Border width - ), - child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor)), - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Set Reminder", - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, - color: AppColors.textColor), - ), - const Text( - "Notify me before the consumption time", - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12, - color: AppColors.textColorLight), - ), - ], + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - ], - ), - Switch( - value: isActiveReminder, - onChanged: (_) {}, - activeColor: Colors.green, + ), ), - ], - ).paddingOnly(left:16, right: 16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, + ), + ], ), + SizedBox(height: 30.h), + ], + ); + } -// Buttons - Row( - children: [ - Expanded( - child: CustomButton( - text: LocaleKeys.checkAvailability.tr(), - fontSize: 14, - onPressed: () async { - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.errorColor, - ), - ), - const SizedBox(width: 12), - Expanded( - child: CustomButton( - text: LocaleKeys.readInstructions.tr(), - fontSize: 14, - onPressed: () async { - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - ), + Widget buildCircleCheckbox(String label) { + final bool isSelected = selectedTimes.contains(label); + return InkWell( + onTap: () { + setState(() { + if (isSelected) { + selectedTimes.remove(label); + } else { + selectedTimes.add(label); + } + }); + }, + borderRadius: BorderRadius.circular(25), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + // Custom circle checkbox + Container( + width: 15.h, + height: 15.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? AppColors.spacerLineColor: AppColors.spacerLineColor, + width: 1.h, ), - ], - ).paddingAll(16), - ], + color: isSelected ? AppColors.errorColor: AppColors.transparent, + ), + ), + SizedBox(width: 12.h), + // Label text + Text( + label, + style: TextStyle(fontSize: 16.fSize, color: Colors.black87), + ), + ], + ), ), ); } + + + void showCircleCheckboxDialog(BuildContext context) async { + final selected = await showDialog>( + context: context, + builder: (context) => const ReminderTimerDialog(), + ); + + if (selected != null && selected.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Reminders set for: ${selected.join(', ').needTranslation}')), + ); + } + } } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 8d233b0..cf769f8 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -463,12 +463,6 @@ class _LandingPageState extends State { Row( children: [ "View all services".toText12(color: AppColors.primaryRedColor).onPress(() { - Navigator.of(context) - .push( - CustomPageRoute( - page: ActiveMedicationPage(), - ), - ); }), SizedBox(width: 2.h), Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 1eff927..4dcf0c4 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -12,6 +12,7 @@ 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/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; @@ -23,6 +24,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/active_medication/active_medication_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; @@ -56,6 +58,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/shimmer/movies_shimmer_widget.dart'; import 'package:provider/provider.dart'; +import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; import '../prescriptions/prescription_detail_page.dart'; import 'widgets/medical_file_appointment_card.dart'; @@ -73,6 +76,7 @@ class _MedicalFilePageState extends State { late MedicalFileViewModel medicalFileViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel; late LabViewModel labViewModel; + late ActivePrescriptionsViewModel activePrescriptionsViewModel; int currentIndex = 0; @@ -98,6 +102,7 @@ class _MedicalFilePageState extends State { myAppointmentsViewModel = Provider.of(context, listen: false); medicalFileViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); + NavigationService navigationService = getIt.get(); return CollapsingListView( title: "Medical File".needTranslation, @@ -528,7 +533,13 @@ class _MedicalFilePageState extends State { Expanded( child: CustomButton( text: "All Medications".needTranslation, - onPressed: () {}, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: ActiveMedicationPage(), + ), + ); + }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 7003a31..4d1b4be 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -14,6 +14,8 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/family_files/family_file_add_widget.dart'; +import '../widgets/medication_reminder/reminder_timer_dialog.dart'; + abstract class DialogService { Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed, Function()? onCancelPressed}); @@ -29,6 +31,8 @@ abstract class DialogService { Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}); Future showAddFamilyFileSheet({String? label, String? message, required Function() onVerificationPress}); + + Future showReminderBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}); // TODO : Need to be Fixed showPhoneNumberPickerSheet ( From Login ADn Signup Bottom Sheet Move Here } @@ -133,6 +137,18 @@ class DialogServiceImp implements DialogService { ); } + @override + Future showReminderBottomSheetWithoutHWithChild({String? label, required String message, Widget? child, required Function() onOkPressed, Function()? onCancelPressed}) async { + final context = navigationService.navigatorKey.currentContext; + if (context == null) return; + showCommonBottomSheetWithoutHeight( + context, + title: label ?? "", + child: child ?? SizedBox(), + callBackFunc: () {}, + ); + } + @override Future showPhoneNumberPickerSheet({String? label, String? message, required Function() onSMSPress, required Function() onWhatsappPress}) async { final context = navigationService.navigatorKey.currentContext; diff --git a/lib/widgets/medication_reminder/reminder_timer_dialog.dart b/lib/widgets/medication_reminder/reminder_timer_dialog.dart new file mode 100644 index 0000000..62dbd96 --- /dev/null +++ b/lib/widgets/medication_reminder/reminder_timer_dialog.dart @@ -0,0 +1,155 @@ +// import 'package:easy_localization/easy_localization.dart'; +// import 'package:flutter/material.dart'; +// import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +// +// import '../../generated/locale_keys.g.dart'; +// import '../../theme/colors.dart'; +// +// class ReminderTimerDialog extends StatefulWidget { +// final Function()? onSetReminderPress; +// final String message; +// +// const ReminderTimerDialog(this.onSetReminderPress, this.message, {super.key}); +// +// +// @override +// State createState() => _ReminderTimerDialogState(); +// } +// +// class _ReminderTimerDialogState extends State { +// final List options = ["Morning", "Afternoon", "Evening", "Midnight"]; +// final List selectedTimes = ["Morning"]; // Default selection +// +// +// @override +// Widget build(BuildContext context) { +// return // +// Column( +// children: [ +// Container( +// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, +// hasShadow: true,), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: CrossAxisAlignment.start, +// // Checkboxes list +// children: options.map((time) => buildCircleCheckbox(time)).toList(), +// ).paddingAll(16), +// ), +// const SizedBox(height: 25), +// // Buttons Row +// Row( +// children: [ +// Expanded( +// child: ElevatedButton.icon( +// onPressed: () => Navigator.pop(context), +// icon: const Icon(Icons.close, color: AppColors.errorColor), +// label: Text( +// LocaleKeys.cancel.tr(), +// style: TextStyle( +// color: AppColors.errorColor, +// fontWeight: FontWeight.w500, +// fontSize: 14 +// ), +// ), +// style: ElevatedButton.styleFrom( +// backgroundColor: AppColors.secondaryLightRedColor, +// elevation: 0, +// padding: const EdgeInsets.symmetric(vertical: 14), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(12), +// ), +// ), +// ), +// ), +// const SizedBox(width: 12), +// Expanded( +// child: ElevatedButton.icon( +// onPressed: () { +// Navigator.pop(context, selectedTimes); +// }, +// icon: const Icon(Icons.notifications_rounded), +// label: Text( +// LocaleKeys.setReminder.tr(), +// style: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 14 +// ), +// ), +// style: ElevatedButton.styleFrom( +// backgroundColor: AppColors.successColor, +// foregroundColor: AppColors.whiteColor, +// elevation: 0, +// padding: const EdgeInsets.symmetric(vertical: 14), +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(12), +// ), +// ), +// ), +// ), +// ], +// ), +// const SizedBox(height: 30), +// ], +// ); +// } +// +// Widget buildCircleCheckbox(String label) { +// final bool isSelected = selectedTimes.contains(label); +// return InkWell( +// onTap: () { +// setState(() { +// if (isSelected) { +// selectedTimes.remove(label); +// } else { +// selectedTimes.add(label); +// } +// }); +// }, +// borderRadius: BorderRadius.circular(25), +// child: Padding( +// padding: const EdgeInsets.symmetric(vertical: 8.0), +// child: Row( +// children: [ +// // Custom circle checkbox +// Container( +// width: 15, +// height: 15, +// decoration: BoxDecoration( +// shape: BoxShape.circle, +// border: Border.all( +// color: isSelected ? AppColors.spacerLineColor: AppColors.spacerLineColor, +// width: 1, +// ), +// color: isSelected ? AppColors.errorColor: AppColors.transparent, +// ), +// ), +// const SizedBox(width: 12), +// // Label text +// Text( +// label, +// style: const TextStyle(fontSize: 16, color: Colors.black87), +// ), +// ], +// ), +// ), +// ); +// } +// +// +// void showCircleCheckboxDialog(BuildContext context) async { +// final selected = await showDialog>( +// context: context, +// builder: (context) => const ReminderTimerDialog(), +// ); +// +// if (selected != null && selected.isNotEmpty) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar(content: Text('Reminders set for: ${selected.join(', ')}')), +// ); +// } +// } +// } +// +// +// From eae16eec44f5a702b59801f4c36209279f780f0b Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Mon, 17 Nov 2025 09:38:20 +0300 Subject: [PATCH 03/12] active medication --- lib/core/api_consts.dart | 3 +- lib/core/dependencies.dart | 12 + lib/core/utils/calendar_utils.dart | 2 +- lib/core/utils/utils.dart | 2 +- .../active_prescriptions_repo.dart | 58 +- .../active_prescriptions_view_model.dart | 168 ++- .../active_prescriptions_response_model.dart | 94 +- lib/main.dart | 4 + .../active_medication_page.dart | 1129 ++++++++++------- .../call_ambulance/tracking_screen.dart | 14 +- .../widgets/nearestERItem.dart | 2 +- lib/presentation/home/landing_page.dart | 11 +- lib/theme/colors.dart | 2 + 13 files changed, 951 insertions(+), 550 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index ed2bba1..6dab797 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -729,7 +729,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -848,6 +848,7 @@ class ApiConsts { static final String getAllSharedRecordsByStatus = 'Services/Authentication.svc/REST/GetAllSharedRecordsByStatus'; static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; + static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; // static values for Api static final double appVersionID = 18.7; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index a82a9ad..67464b3 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -3,6 +3,8 @@ import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_repo.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; @@ -45,6 +47,8 @@ import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../features/active_prescriptions/active_prescriptions_repo.dart'; + GetIt getIt = GetIt.instance; class AppDependencies { @@ -103,6 +107,7 @@ class AppDependencies { getIt.registerLazySingleton(() => MedicalFileRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ImmediateLiveCareRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => EmergencyServicesRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton @@ -202,6 +207,13 @@ class AppDependencies { ), ); + getIt.registerLazySingleton( + () => ActivePrescriptionsViewModel( + errorHandlerService: getIt(), + activePrescriptionsRepo: getIt() + ), + ); + // Screen-specific VMs → Factory // getIt.registerFactory( // () => BookAppointmentsViewModel( diff --git a/lib/core/utils/calendar_utils.dart b/lib/core/utils/calendar_utils.dart index 2068db9..8c0db18 100644 --- a/lib/core/utils/calendar_utils.dart +++ b/lib/core/utils/calendar_utils.dart @@ -266,7 +266,7 @@ setCalender(BuildContext context, eventId: eventId + (i.toString() + j.toString()), location: '', //event id with varitions ); - + print("Creating event #$j for day $i → $actualDate"); actualDate = DateTime(actualDate.year, actualDate.month, actualDate.day, 8, 0); } actualDate = Jiffy.parseFromDateTime(actualDate).add(days: 1).dateTime; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index a88d9c2..1fc3806 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -670,7 +670,7 @@ class Utils { } /// Widget to build an SVG from network - static Widget buildImgWithNetwork({required String url, required Color iconColor, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover, ImageErrorWidgetBuilder? errorBuilder}) { + static Widget buildImgWithNetwork({required String url, bool isDisabled = false, double width = 24, double height = 24, BoxFit fit = BoxFit.cover, ImageErrorWidgetBuilder? errorBuilder}) { return Image.network( url, width: width, diff --git a/lib/features/active_prescriptions/active_prescriptions_repo.dart b/lib/features/active_prescriptions/active_prescriptions_repo.dart index 847d6e8..437f364 100644 --- a/lib/features/active_prescriptions/active_prescriptions_repo.dart +++ b/lib/features/active_prescriptions/active_prescriptions_repo.dart @@ -2,7 +2,6 @@ import 'package:dartz/dartz.dart'; import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; - import '../../core/api/api_client.dart'; import '../../core/api_consts.dart'; import '../../core/common_models/generic_api_model.dart'; @@ -11,7 +10,7 @@ import '../../services/logger_service.dart'; abstract class ActivePrescriptionsRepo { - Future>> getActivePrescriptionsDetails(); + Future>>> getActivePrescriptionsDetails(); } @@ -23,10 +22,10 @@ class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { @override - Future>> getActivePrescriptionsDetails() async + Future>>> getActivePrescriptionsDetails() async { try { - GenericApiModel? apiResponse; + GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( ApiConsts.getActivePrescriptionsDetails, @@ -36,18 +35,20 @@ class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - // final list = response['GetActivePrescriptionReportByPatientIDList']; - - // final prescriptionLists = list.map((item) => ActivePrescriptionsResponseModel.fromJson(item as Map)).toList().cast(); + var list = response['List_ActiveGetPrescriptionReportByPatientID']; + var res = list + .map( + (item) => ActivePrescriptionsResponseModel.fromJson(item)) + .toList(); - apiResponse = GenericApiModel( + apiResponse = GenericApiModel>( messageStatus: messageStatus, statusCode: statusCode, errorMessage: null, - data: response, + // data: response, + data: res ); - return ['List_ActiveGetPrescriptionReportByPatientID']; - //apiResponse; + return apiResponse; } catch (e) { failure = DataParsingFailure(e.toString()); } @@ -61,39 +62,4 @@ class ActivePrescriptionsRepoImp implements ActivePrescriptionsRepo { } } - - - - // - // Future> getActiveMedications() { - // try { - // GenericApiModel? apiResponse; - // Failure? failure; - // return apiClient.post( - // ApiConsts.getActivePrescriptionsDetails, - // body: patientDeviceDataRequest, - // onFailure: (error, statusCode, {messageStatus, failureType}) { - // failure = failureType; - // }, - // onSuccess: (response, statusCode, {messageStatus, errorMessage}) { - // try { - // apiResponse = GenericApiModel( - // messageStatus: messageStatus, - // statusCode: statusCode, - // errorMessage: errorMessage, - // data: response, - // ); - // } catch (e) { - // failure = DataParsingFailure(e.toString()); - // } - // }, - // ).then((_) { - // if (failure != null) return Left(failure!); - // if (apiResponse == null) return Left(ServerFailure("Unknown error")); - // return Right(apiResponse!); - // }); - // } catch (e) { - // return Future.value(Left(UnknownFailure(e.toString()))); - // } - // } } \ No newline at end of file diff --git a/lib/features/active_prescriptions/active_prescriptions_view_model.dart b/lib/features/active_prescriptions/active_prescriptions_view_model.dart index d995f96..03f84ea 100644 --- a/lib/features/active_prescriptions/active_prescriptions_view_model.dart +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -4,13 +4,17 @@ import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_ import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; -class ActivePrescriptionsViewModel extends ChangeNotifier { +class ActivePrescriptionsViewModel extends ChangeNotifier { bool isActivePrescriptionsDetailsLoading = false; - late ActivePrescriptionsRepo activePrescriptionsRepo; + late ActivePrescriptionsRepo activePrescriptionsRepo; late ErrorHandlerService errorHandlerService; - // Prescription Orders Lists + ActivePrescriptionsViewModel({ + required this.activePrescriptionsRepo, + required this.errorHandlerService, + }); + List activePrescriptionsDetailsList = []; initActivePrescriptionsViewModel() { @@ -20,38 +24,172 @@ class ActivePrescriptionsViewModel extends ChangeNotifier { setPrescriptionsDetailsLoading() { isActivePrescriptionsDetailsLoading = true; - // activePrescriptionsDetailsList.clear(); notifyListeners(); } - Future getActiveMedications( {Function(dynamic)? onSuccess, Function(String)? onError}) - async { + // Get medications list + Future getActiveMedications({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { final result = await activePrescriptionsRepo.getActivePrescriptionsDetails(); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), (apiResponse) { - if (apiResponse.messageStatus == 2) { - // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); - } else if (apiResponse.messageStatus == 1) { - activePrescriptionsDetailsList = apiResponse.data!; - isActivePrescriptionsDetailsLoading = false; + if (apiResponse.messageStatus == 1) { + activePrescriptionsDetailsList = apiResponse.data ?? []; notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); - print(activePrescriptionsDetailsList.length); - } + if (onSuccess != null) onSuccess(apiResponse.data); } }, ); } + DateTime parseDate(String? date) { + if (date == null) return DateTime.now(); + final regex = RegExp(r"\/Date\((\d+)([+-]\d+)?\)\/"); + final match = regex.firstMatch(date); + if (match != null) { + final millis = int.parse(match.group(1)!); + return DateTime.fromMillisecondsSinceEpoch(millis); + } + return DateTime.tryParse(date) ?? DateTime.now(); + } + // Extract numeric value ( "3 / week" → 3) + int extractNumberFromFrequency(String? frequency) { + if (frequency == null) return 1; + final m = RegExp(r'(\d+)').firstMatch(frequency); + if (m != null) return int.tryParse(m.group(1)!) ?? 1; + return 1; + } + // Generate medication days based on frequency text + List generateMedicationDays(ActivePrescriptionsResponseModel med) { + final start = parseDate(med.startDate); + final duration = med.days ?? 0; + final frequency = (med.frequency ?? "").toLowerCase().trim(); + + List result = []; + if (duration <= 0) return result; + + // Every N hours ( "Every Six Hours", "Every 8 hours") + if (frequency.contains("hour")) { + final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); + int intervalHours = 0; + + if (match != null) { + intervalHours = int.tryParse(match.group(1)!) ?? 0; + } else { + // handle text numbers like "Every six hours" + final textNum = { + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "ten": 10, + "twelve": 12, + }; + for (var key in textNum.keys) { + if (frequency.contains(key)) { + intervalHours = textNum[key]!; + break; + } + } + } + if (intervalHours > 0) { + for (int day = 0; day < duration; day++) { + final dayStart = start.add(Duration(days: day)); + for (int hour = 0; hour < 24; hour += intervalHours) { + result.add(DateTime(dayStart.year, dayStart.month, dayStart.day, hour)); + } + } + return result; + } + } + + // Daily (every day) + if (frequency.contains("day") && + !frequency.contains("every other") && + !frequency.contains("every ")) { + for (int i = 0; i < duration; i++) { + result.add(start.add(Duration(days: i))); + } + } + + // Every other day + else if (frequency.contains("every other day")) { + for (int i = 0; i < duration; i += 2) { + result.add(start.add(Duration(days: i))); + } + } + + // Every N days → e.g. "Every 3 days", "Every 5 days" + else if (frequency.contains("every") && frequency.contains("day")) { + final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); + final interval = match != null ? int.tryParse(match.group(1)!) ?? 1 : 1; + for (int i = 0; i < duration; i += interval) { + result.add(start.add(Duration(days: i))); + } + } + + // Once or twice a week + else if (frequency.contains("once a week")) { + for (int i = 0; i < duration; i += 7) { + result.add(start.add(Duration(days: i))); + } + } else if (frequency.contains("twice a week")) { + for (int i = 0; i < duration; i += 3) { + result.add(start.add(Duration(days: i))); + } + } + + // Numeric frequency like "3 / week", "2 / week" + else if (frequency.contains("week")) { + int timesPerWeek = extractNumberFromFrequency(frequency); + double interval = 7 / timesPerWeek; + double dayPointer = 0; + + for (int i = 0; i < duration; i++) { + if (i >= dayPointer.floor()) { + result.add(start.add(Duration(days: i))); + dayPointer += interval; + } + } + } + + else { + result.add(start); + } + final unique = {}; + for (final d in result) { + unique["${d.year}-${d.month}-${d.day}"] = d; + } + return unique.values.toList()..sort((a, b) => a.compareTo(b)); + } + bool sameYMD(DateTime a, DateTime b) => + a.year == b.year && a.month == b.month && a.day == b.day; + // Filter medications for selected day + List getMedsForSelectedDay(DateTime selectedDate) { + final target = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + return activePrescriptionsDetailsList.where((med) { + final days = generateMedicationDays(med); + return days.any((d) => sameYMD(d, target)); + }).toList(); + } } + + + diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart index 878e191..eb216a6 100644 --- a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -7,7 +7,7 @@ class ActivePrescriptionsResponseModel { dynamic companyName; int? days; dynamic doctorName; - int? doseDailyQuantity; + int? doseDailyQuantity; // doses per day String? frequency; int? frequencyNumber; dynamic image; @@ -23,7 +23,7 @@ class ActivePrescriptionsResponseModel { dynamic patientName; dynamic phoneOffice1; dynamic prescriptionQr; - int? prescriptionTimes; + dynamic prescriptionTimes; dynamic productImage; String? productImageBase64; String? productImageString; @@ -35,6 +35,10 @@ class ActivePrescriptionsResponseModel { int? scaleOffset; String? startDate; + // ✅ Added for reminder feature + List selectedDoseTimes = []; + bool isReminderOn = false; // toggle status + ActivePrescriptionsResponseModel({ this.address, this.appointmentNo, @@ -69,47 +73,57 @@ class ActivePrescriptionsResponseModel { this.sku, this.scaleOffset, this.startDate, - }); - factory ActivePrescriptionsResponseModel.fromRawJson(String str) => ActivePrescriptionsResponseModel.fromJson(json.decode(str)); + // ✅ Default values for new fields (won’t break API) + List? selectedDoseTimes, + this.isReminderOn = false, + }) : selectedDoseTimes = selectedDoseTimes ?? []; + + factory ActivePrescriptionsResponseModel.fromRawJson(String str) => + ActivePrescriptionsResponseModel.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); - factory ActivePrescriptionsResponseModel.fromJson(Map json) => ActivePrescriptionsResponseModel( - address: json["Address"], - appointmentNo: json["AppointmentNo"], - clinic: json["Clinic"], - companyName: json["CompanyName"], - days: json["Days"], - doctorName: json["DoctorName"], - doseDailyQuantity: json["DoseDailyQuantity"], - frequency: json["Frequency"], - frequencyNumber: json["FrequencyNumber"], - image: json["Image"], - imageExtension: json["ImageExtension"], - imageSrcUrl: json["ImageSRCUrl"], - imageString: json["ImageString"], - imageThumbUrl: json["ImageThumbUrl"], - isCovered: json["IsCovered"], - itemDescription: json["ItemDescription"], - itemId: json["ItemID"], - orderDate: json["OrderDate"], - patientId: json["PatientID"], - patientName: json["PatientName"], - phoneOffice1: json["PhoneOffice1"], - prescriptionQr: json["PrescriptionQR"], - prescriptionTimes: json["PrescriptionTimes"], - productImage: json["ProductImage"], - productImageBase64: json["ProductImageBase64"], - productImageString: json["ProductImageString"], - projectId: json["ProjectID"], - projectName: json["ProjectName"], - remarks: json["Remarks"], - route: json["Route"], - sku: json["SKU"], - scaleOffset: json["ScaleOffset"], - startDate: json["StartDate"], - ); + factory ActivePrescriptionsResponseModel.fromJson(Map json) => + ActivePrescriptionsResponseModel( + address: json["Address"], + appointmentNo: json["AppointmentNo"], + clinic: json["Clinic"], + companyName: json["CompanyName"], + days: json["Days"], + doctorName: json["DoctorName"], + doseDailyQuantity: json["DoseDailyQuantity"], + frequency: json["Frequency"], + frequencyNumber: json["FrequencyNumber"], + image: json["Image"], + imageExtension: json["ImageExtension"], + imageSrcUrl: json["ImageSRCUrl"], + imageString: json["ImageString"], + imageThumbUrl: json["ImageThumbUrl"], + isCovered: json["IsCovered"], + itemDescription: json["ItemDescription"], + itemId: json["ItemID"], + orderDate: json["OrderDate"], + patientId: json["PatientID"], + patientName: json["PatientName"], + phoneOffice1: json["PhoneOffice1"], + prescriptionQr: json["PrescriptionQR"], + prescriptionTimes: json["PrescriptionTimes"], + productImage: json["ProductImage"], + productImageBase64: json["ProductImageBase64"], + productImageString: json["ProductImageString"], + projectId: json["ProjectID"], + projectName: json["ProjectName"], + remarks: json["Remarks"], + route: json["Route"], + sku: json["SKU"], + scaleOffset: json["ScaleOffset"], + startDate: json["StartDate"], + + // ✅ Ensure local reminder values are not overwritten by API + selectedDoseTimes: [], + isReminderOn: false, + ); Map toJson() => { "Address": address, @@ -145,5 +159,7 @@ class ActivePrescriptionsResponseModel { "SKU": sku, "ScaleOffset": scaleOffset, "StartDate": startDate, + + }; } diff --git a/lib/main.dart b/lib/main.dart index 259ce3b..5a33d02 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.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/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; @@ -129,6 +130,9 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index 6394002..aa2abe5 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -1,355 +1,538 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -// import 'package:sizer/sizer.dart'; -import '../../core/dependencies.dart'; +import 'package:flutter/cupertino.dart'; +import '../../core/app_assets.dart'; +import '../../core/utils/calendar_utils.dart'; import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; import '../../features/active_prescriptions/models/active_prescriptions_response_model.dart'; import '../../generated/locale_keys.g.dart'; -import '../../services/dialog_service.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import 'package:intl/intl.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -// import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; - import '../../widgets/buttons/custom_button.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting import 'package:provider/provider.dart'; - +import '../../widgets/loader/bottomsheet_loader.dart'; class ActiveMedicationPage extends StatefulWidget { - //inal List activePrescriptionsResponseModel; - - ActiveMedicationPage({super.key, }); - - - + const ActiveMedicationPage({super.key}); @override State createState() => _ActiveMedicationPageState(); } class _ActiveMedicationPageState extends State { - late DateTime currentDate; - late DateTime selectedDate; - - + late DateTime currentDate; + late DateTime selectedDate; + List selectedDayMeds = []; ActivePrescriptionsViewModel? activePreVM; + Map medReminderStatus = {}; @override - void initState() { - activePreVM = Provider.of(context, listen: false); - activePreVM?.getActiveMedications(); - print(activePreVM?.activePrescriptionsDetailsList); + void initState() { super.initState(); currentDate = DateTime.now(); selectedDate = currentDate; + WidgetsBinding.instance.addPostFrameCallback((_) async { + activePreVM = Provider.of(context, listen: false); + LoaderBottomSheet.showLoader(); + await activePreVM!.getActiveMedications( + onSuccess: (_) { + LoaderBottomSheet.hideLoader(); + final todayMeds = activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() { + selectedDayMeds = todayMeds; + }); + }, + onError: (_) { + LoaderBottomSheet.hideLoader(); + }, + ); + activePreVM!.addListener(() { + if (!mounted) return; + final medsForDay = activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() => selectedDayMeds = medsForDay); + }); + }); } -// Generate today + next 6 days - List getUpcomingDays() { - return List.generate(7, (index) => currentDate.add(Duration(days: index))); - } - // on/off toggle - bool isOn = true; - - get index => null; + List getUpcomingDays() => List.generate(7, (index) => currentDate.add(Duration(days: index))); @override Widget build(BuildContext context) { - // activePreVM = Provider.of(context, listen: false); - List days = getUpcomingDays(); - int dayIndex = selectedDate.difference(currentDate).inDays; - String dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)} "; - return Scaffold( + final days = getUpcomingDays(); + final dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)}"; + return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( - onBackPressed: () { - Navigator.of(context).pop(); - }, - onLanguageChanged: (lang) {}, - hideLogoAndLang: true, + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 65.h, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: days.length, - // itemCount: widget.details.length, - itemBuilder: (context, index) { - DateTime day = days[index]; - String label = DateFormat('E').format(day); // Mon, Tue - return Padding( - padding: const EdgeInsets.only(right: 12), - child: buildDayCard(label, day), - ); - }, - ), - ), - SizedBox(height: 20.h), - -// Show full date text - Text( - dateText, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Active Medications".needTranslation, style: TextStyle( color: AppColors.textColor, - fontSize: 16.fSize, - fontWeight: FontWeight.w500), + fontSize: 27.f, + fontWeight: FontWeight.w600)), + SizedBox(height: 16.h), + SizedBox( + height: 65.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: days.length, + itemBuilder: (context, index) { + final day = days[index]; + final label = DateFormat('E').format(day); + return Padding( + padding: const EdgeInsets.only(right: 12), + child: buildDayCard(label, day), + ); + }, ), - Text( - "Medications".needTranslation, - style: TextStyle( - color: AppColors.primaryRedBorderColor,fontSize: 12.fSize, fontWeight: FontWeight.w500), - ), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, - hasShadow: true,), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + SizedBox(height: 20.h), + RichText( + text: TextSpan( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(1), - child: Container( - width: 59.h, - height: 59.h, - decoration: BoxDecoration( - border: Border.all( - color: AppColors.spacerLineColor,// Border color - width: 1.0.h, ), - borderRadius: BorderRadius.circular(30),// Border width - ), - child: - Utils.buildImgWithNetwork(url: activePreVM!.activePrescriptionsDetailsList[index].productImageString.toString(),width: 26.h,) - ), - ), - SizedBox(width: 12.h), - Text( - activePreVM!.activePrescriptionsDetailsList[index].itemDescription.toString(), - style: TextStyle( - fontSize: 16.fSize, - height: 1.2.h, - fontWeight: FontWeight.w700, - color: Colors.black87), - ), - ], - ), - SizedBox(height: 12.h), - activePreVM!.activePrescriptionsDetailsList.length > 0 ? - Wrap( - direction: Axis.horizontal, - spacing: 4.h, - runSpacing: 4.h, - children: [ - AppCustomChipWidget( - labelText: "Route: ${activePreVM?.activePrescriptionsDetailsList[index].route}", - ), - AppCustomChipWidget( - labelText: "Frequency: ${activePreVM?.activePrescriptionsDetailsList[index].frequency}".needTranslation, - ), - AppCustomChipWidget( - labelText: "Daily Does ${activePreVM?.activePrescriptionsDetailsList[index].doseDailyQuantity}".needTranslation, - ), - AppCustomChipWidget( - labelText: "Duration: ${activePreVM?.activePrescriptionsDetailsList[index].days} ".needTranslation, - ), - ], - ): - Container( - child: Text("no data"), - ), - SizedBox(height: 12.h), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.info_outline, color: Colors.grey,), - SizedBox(width: 8.h), - Expanded( - child: Text( - "Remark: some remarks about the prescription will be here".needTranslation, - style: TextStyle( - fontSize: 10.fSize, - color: AppColors.greyTextColor, - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.visible, - ), - ) - ], - ), - ], - ).paddingAll(16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, + TextSpan( + text: "${selectedDate.day}", + style: TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), ), - // Reminder Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 40.h, - height: 40.h, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10),// Border width - ), - child: Icon(Icons.notifications_sharp, color: AppColors.greyTextColor) - // MedicalFileCard( - // label: "Vaccine Info".needTranslation, - // textColor: AppColors.blackColor, - // backgroundColor: AppColors.whiteColor, - // svgIcon: AppAssets..bell, - // isLargeText: true, - // iconSize: 36.h, - // ) + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -4), + child: Text( + getSuffix(selectedDate.day), + style: const TextStyle( + fontSize: 12, + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), ), - SizedBox(width: 8.h), - Expanded( + ), + ), + TextSpan( + text: " ${DateFormat.MMMM().format(selectedDate)}", + style: const TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + Text("Medications".needTranslation, + style: TextStyle( + color: AppColors.primaryRedBorderColor, + fontSize: 12.f, + fontWeight: FontWeight.w500)), + SizedBox(height: 16.h), + Expanded( + child: SingleChildScrollView( + child: selectedDayMeds.isNotEmpty + ? ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: selectedDayMeds.length, + itemBuilder: (context, index) { + final med = selectedDayMeds[index]; + final doses = med.doseDailyQuantity ?? 1; + med.selectedDoseTimes ??= List.filled(doses, null); + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + margin: EdgeInsets.all(10), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Set Reminder", - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14.fSize, - color: AppColors.textColor), - ), - Text( - "Notify me before the consumption time", - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 12.fSize, - color: AppColors.textColorLight), + _buildMedHeader(med), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + color: AppColors.lightGreyTextColor, + size: 20, + ), + SizedBox(width: 6.h), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Remarks: ".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.w600, + fontSize: 10, + ), + ), + TextSpan( + text: "some remarks about the prescription will be here".needTranslation, + style: TextStyle( + color: AppColors.lightGreyTextColor, + fontWeight: FontWeight.normal, + fontSize: 10, + ), + ), + ], + ), + ), + ), + ], + ).paddingOnly(left: 16, right: 16), + const Divider(color: AppColors.greyColor), + // Reminder Section + GestureDetector( + onTap: () => showDoseDialog(med, index), + child: Row( + children: [ + Container( + width: 40.h, + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(10), + ), + child: Utils.buildSvgWithAssets( + icon: AppAssets.bell, + height: 24.h, + width: 24.h, + iconColor: AppColors.greyTextColor, + ), + ), + SizedBox(width: 12.h), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text("Set Reminder".needTranslation, + style: TextStyle( + fontSize: 14.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor)), + Text("Notify me before the consumption time".needTranslation, + style: TextStyle( + fontSize: 12.f, + color: AppColors.textColorLight, + )), + ], + ), + ), + _buildToggle(index) + ], + ).paddingAll(16), ), + const Divider(color: AppColors.greyColor), + _buildButtons(), ], - ).onPress(() { - DialogService dialogService = getIt.get(); - dialogService.showReminderBottomSheetWithoutHWithChild( - label: "Set the timer for reminder".needTranslation, - message: "", - child: ReminderTimerDialog(), - onOkPressed: () {}, - ); - }), + ), + ); + }, + ) + : Utils.getNoDataWidget(context, + noDataText: "No medications today".needTranslation), + ), + ), + ], + ).paddingAll(16), + ); + } + + //medicine card + Widget _buildMedHeader(ActivePrescriptionsResponseModel med) => Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: 59.h, + height: 59.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.spacerLineColor, + borderRadius: 30.r, + hasShadow: false, + ), + child: Utils.buildImgWithNetwork( + url: med.productImageString ?? "" ).circle(52.h) + ), + ), + SizedBox(width: 12.h), + Expanded( + child: Text( + med.itemDescription ?? "", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor), + ), + ), + ]), + SizedBox(height: 12.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget(labelText: "Route: ${med.route}".needTranslation), + AppCustomChipWidget(labelText: "Frequency: ${med.frequency}".needTranslation), + AppCustomChipWidget(labelText: "Daily Dose: ${med.doseDailyQuantity}".needTranslation), + AppCustomChipWidget(labelText: "Duration: ${med.days}".needTranslation), + ], + ), + ], + ), + ); + + Widget _buildButtons() => Padding( + padding: EdgeInsets.all(16), + child: Row(children: [ + Expanded( + child: CustomButton( + text: "Check Availability".needTranslation, + fontSize: 13.f, + onPressed: () {}, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + ), + ), + SizedBox(width: 12.h), + Expanded( + child: CustomButton( + text: "Read Instructions".needTranslation, fontSize: 13.f, onPressed: () {})), + ]), + ); + + Widget _buildToggle(int index) { + final value = medReminderStatus[index] ?? false; + + return GestureDetector( + onTap: () async { + await showDoseDialog(selectedDayMeds[index], index); + setState(() { + if ((selectedDayMeds[index].selectedDoseTimes ?? []).any((t) => t != null)) { + medReminderStatus[index] = true; + } + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: value ? AppColors.lightGreenColor : AppColors.greyColor.withOpacity(0.3), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: value ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: value ? AppColors.textGreenColor : AppColors.greyTextColor, + ), + ), + ), + ), + ), + ); + } + + Future showDoseDialog(ActivePrescriptionsResponseModel med, int medIndex) { + final doses = med.frequencyNumber ?? 1; + if (med.selectedDoseTimes.length != doses) { + med.selectedDoseTimes = List.generate(doses, (_) => null); + } + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 520.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + hasShadow: true, + ), + + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Reminders".needTranslation, + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, ), - GestureDetector( - onTap: () { - setState(() { - isOn = !isOn; - }); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 50.h, - height: 28.h, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: isOn ? AppColors.lightGreenColor: AppColors.greyColor, ), - child: AnimatedAlign( - duration: const Duration(milliseconds: 200), - alignment: isOn ? Alignment.centerRight : Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(3), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close, color:AppColors.blackBgColor), + ), + ], + ), + SizedBox(height: 20.h), + Expanded( + child: ListView.builder( + itemCount: doses, + itemBuilder: (context, doseIndex) { + final badgeColor = [ + AppColors.textGreenColor, + AppColors.infoColor, + AppColors.labelColorYellow, + AppColors.purpleBg + ][doseIndex % 4]; + + final doseLabel = "${doseIndex + 1}${getSuffix(doseIndex + 1)}"; + final time = med.selectedDoseTimes[doseIndex] ?? "Not set yet"; + + return GestureDetector( + onTap: () { + Navigator.pop(context); + showTimePickerSheet(med, medIndex, doseIndex); + }, child: Container( - width: 22.h, - height: 22.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isOn ? AppColors.textGreenColor : AppColors.greyTextColor, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 6, horizontal: 14), + decoration: BoxDecoration( + color: badgeColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + doseLabel, + style: TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Set reminder for $doseLabel dose", + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + ), + Icon(Icons.arrow_forward_outlined, + size: 24.w, color: AppColors.textColor), + ], + ), + SizedBox(height: 4.h), + Text( + time, + style: TextStyle( + fontSize: 12.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + ), + ], ), ), - ), - ), + ); + }, ), ), - SizedBox(width: 2.h), - // Switch( - // value: isOn, - // onChanged: (value){ - // setState(() { - // isOn = value; - // }); - // }, - // activeColor: AppColors.lightGreenColor, - // activeTrackColor: AppColors.lightGreenColor, - // activeThumbColor: AppColors.textGreenColor, - // inactiveThumbColor: AppColors.greyTextColor, - // inactiveTrackColor: AppColors.greyColor, - // ), - ], - ).paddingAll(16), - const Divider( - indent: 0, - endIndent: 0, - thickness: 1, - color: AppColors.greyColor, - ), - -// Buttons - Row( - children: [ - Expanded( - child: CustomButton( - text: LocaleKeys.checkAvailability.tr(), - fontSize: 14.fSize, - onPressed: () async { - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.errorColor, - ), - ), - SizedBox(width: 12.h), - Expanded( - child: CustomButton( - text: LocaleKeys.readInstructions.tr(), - fontSize: 14.fSize, - onPressed: () async { - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - ), - ), - ], - ).paddingAll(16), - ], - ), - ) - ] - ), + ], + ), + ), + ), + ); + } + void showTimePickerSheet( + ActivePrescriptionsResponseModel med, int medIndex, int doseIndex) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 460.h, + decoration: BoxDecoration( + color: AppColors.bottomSheetBgColor, + borderRadius: + BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + ), + child: ReminderTimerDialog( + med: med, + frequencyNumber: med.doseDailyQuantity ?? 1, + doseIndex: doseIndex, + onTimeSelected: (String time) { + setState(() { + med.selectedDoseTimes[doseIndex] = time; + medReminderStatus[medIndex] = true; + }); + }, + ), ), ); } - Widget buildDayCard(String label, DateTime date,) { - bool isSelected = selectedDate.day == date.day && + Widget buildDayCard(String label, DateTime date) { + final isSelected = selectedDate.day == date.day && selectedDate.month == date.month && selectedDate.year == date.year; - return GestureDetector( onTap: () { + final vm = + Provider.of(context, listen: false); setState(() { selectedDate = date; + selectedDayMeds = vm.getMedsForSelectedDay(date); }); }, child: Container( @@ -357,36 +540,38 @@ class _ActiveMedicationPageState extends State { height: 65.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: isSelected ? AppColors.secondaryLightRedBorderColor: AppColors.transparent, + color: isSelected + ? AppColors.secondaryLightRedBorderColor + : Colors.transparent, border: Border.all( - color: isSelected ? AppColors.primaryRedBorderColor : AppColors.spacerLineColor, - width: 1.0.h, - ), + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.spacerLineColor, + width: 1), ), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - date.day == currentDate.day ? "Today".needTranslation : label, - style: TextStyle( - color: isSelected ? AppColors.primaryRedBorderColor : AppColors.greyTextColor, - fontSize: 12.fSize, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 5.h), - Text( - date.day.toString(), - style: TextStyle( - fontSize: 16.fSize, - fontWeight: FontWeight.bold, - color: isSelected ? AppColors.primaryRedBorderColor : AppColors.textColor, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + date.day == currentDate.day ? "Today" : label, + style: TextStyle( + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.greyTextColor, + fontSize: 11.f, + fontWeight: FontWeight.w500), ), - ), - ], - ), + SizedBox(height: 5.h), + Text("${date.day}", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.bold, + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.textColor)) + ]), ), ), ); @@ -398,167 +583,235 @@ class _ActiveMedicationPageState extends State { if (day == 3 || day == 23) return "rd"; return "th"; } - - // Widget manageReminder(){ - // NavigationService navigationService = getIt(); - // return Container( - // width: 59, - // height: 59, - // decoration: BoxDecoration( - // border: Border.all( - // color: AppColors.spacerLineColor,// Border color - // width: 1.0, ), - // borderRadius: BorderRadius.circular(30),// Border width - // ), - // child: - // Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon,width: 30.h, height: 30.h) - // ); - // } } + + class ReminderTimerDialog extends StatefulWidget { - // final Function()? onSetReminderPress; - // final String message; - // - // const ReminderTimerDialog(this.onSetReminderPress, this.message, {super.key}); - const ReminderTimerDialog({super.key}); + final int frequencyNumber; + final int doseIndex; + final Function(String) onTimeSelected; + final ActivePrescriptionsResponseModel med; + + const ReminderTimerDialog({ + super.key, + required this.frequencyNumber, + required this.doseIndex, + required this.onTimeSelected, + required this.med, + }); @override State createState() => _ReminderTimerDialogState(); } class _ReminderTimerDialogState extends State { - final List options = ["Morning", "Afternoon", "Evening", "Midnight"]; - final List selectedTimes = ["Morning"]; // Default selection + TimeOfDay selectedTime = TimeOfDay.now(); + String? _selectedTime; + String bigTimeText = "00:00"; + bool showPicker = false; + final List> presetTimes = [ + ["06:00 AM", "07:00 AM", "08:00 AM", "09:00 AM"], // Morning + ["12:00 PM", "01:00 PM", "02:00 PM", "03:00 PM"], // Noon + ["06:00 PM", "07:00 PM", "08:00 PM", "09:00 PM"], // Evening + ]; @override Widget build(BuildContext context) { - return // - Column( - children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24, - hasShadow: true,), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - // Checkboxes list - children: options.map((time) => buildCircleCheckbox(time)).toList(), - ).paddingAll(16), - ), - SizedBox(height: 25.h), - // Buttons Row - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close, color: AppColors.errorColor), - label: Text( - LocaleKeys.cancel.tr(), - style: TextStyle( - color: AppColors.errorColor, - fontWeight: FontWeight.w500, - fontSize: 14.fSize + final int bucket = widget.doseIndex.clamp(0, 2); + final List times = presetTimes[bucket]; + return Padding( + padding: const EdgeInsets.all(16), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Time for ${widget.doseIndex + 1} dose".needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.bold), + ), + SizedBox(height: 12.h), + // Preset times + Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + children: times.map((t) { + bool selected = _selectedTime == t; + return AppCustomChipWidget( + labelText: t, + backgroundColor: selected + ? AppColors.lightGreenButtonColor + : AppColors.transparent, + textColor: AppColors.textColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: selected + ? AppColors.successColor + : AppColors.spacerLineColor, + width: 1.2, ), + borderRadius: BorderRadius.circular(12), ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.secondaryLightRedColor, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 14), + onChipTap: () { + setState(() { + _selectedTime = t; + selectedTime = _parseTime(t); + bigTimeText = selectedTime.format(context).split(" ")[0]; + showPicker = false; + }); + }, + ); + }).toList(), + ), + SizedBox(height: 25.h), + GestureDetector( + onTap: () { + setState(() { + showPicker = !showPicker; + }); + }, + child: Center( + child: Column( + children: [ + Text( + bigTimeText, + style: TextStyle( + fontSize: 48.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor + ), ), - ), + Text( + selectedTime.period == DayPeriod.am ? "AM" : "PM", + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.bold, + color: AppColors.greyTextColor, + ), + ), + ], ), ), - SizedBox(width: 12.h), - Expanded( - child: ElevatedButton.icon( - onPressed: () { - Navigator.pop(context, selectedTimes); + ), + SizedBox(height: 15.h), + // Time picker + if (showPicker) + SizedBox( + height: 100.h, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: false, + initialDateTime: DateTime( + 2024, + 1, + 1, + selectedTime.hour, + selectedTime.minute, + ), + onDateTimeChanged: (DateTime newTime) { + setState(() { + _selectedTime = null; + selectedTime = TimeOfDay( + hour: newTime.hour, + minute: newTime.minute, + ); + bigTimeText = + selectedTime.format(context).split(" ")[0]; + }); }, - icon: const Icon(Icons.notifications_rounded), - label: Text( - LocaleKeys.setReminder.tr(), - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14.fSize + ), + ), + SizedBox(height: 25.h), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.successColor, - foregroundColor: AppColors.whiteColor, - elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + onPressed: () async { + final selectedFormattedTime = + selectedTime.format(context); + widget.onTimeSelected(selectedFormattedTime); + try { + final parts = selectedFormattedTime.split(":"); + int hour = int.parse(parts[0]); + int minute = int.parse(parts[1].split(" ")[0]); + bool isPM = selectedFormattedTime.contains("PM"); + if (isPM && hour != 12) hour += 12; + if (!isPM && hour == 12) hour = 0; + int totalMinutes = hour * 60 + minute; + // Call setCalender() + await setCalender( + context, + eventId: widget.med.itemId.toString(), + selectedMinutes: totalMinutes, + frequencyNumber: widget.frequencyNumber, + days: widget.med.days ?? 1, + orderDate: widget.med.orderDate ?? "", + itemDescriptionN: widget.med.itemDescription ?? "", + route: widget.med.route ?? "", + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Reminder added to calendar ✅".needTranslation)), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text("Error while setting calendar: $e".needTranslation)), + ); + } + Navigator.pop(context); + }, + child: Text( + LocaleKeys.save.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.f, + ), ), ), ), - ), - ], - ), - SizedBox(height: 30.h), - ], - ); - } - - Widget buildCircleCheckbox(String label) { - final bool isSelected = selectedTimes.contains(label); - return InkWell( - onTap: () { - setState(() { - if (isSelected) { - selectedTimes.remove(label); - } else { - selectedTimes.add(label); - } - }); - }, - borderRadius: BorderRadius.circular(25), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - // Custom circle checkbox - Container( - width: 15.h, - height: 15.h, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: isSelected ? AppColors.spacerLineColor: AppColors.spacerLineColor, - width: 1.h, - ), - color: isSelected ? AppColors.errorColor: AppColors.transparent, - ), - ), - SizedBox(width: 12.h), - // Label text - Text( - label, - style: TextStyle(fontSize: 16.fSize, color: Colors.black87), + ], ), ], - ), + ).paddingAll(16), ), ); } - void showCircleCheckboxDialog(BuildContext context) async { - final selected = await showDialog>( - context: context, - builder: (context) => const ReminderTimerDialog(), - ); - - if (selected != null && selected.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Reminders set for: ${selected.join(', ').needTranslation}')), - ); + TimeOfDay _parseTime(String t) { + try { + int hour = int.parse(t.split(":")[0]); + int minute = int.parse(t.split(":")[1].split(" ")[0]); + bool pm = t.contains("PM"); + if (pm && hour != 12) hour += 12; + if (!pm && hour == 12) hour = 0; + return TimeOfDay(hour: hour, minute: minute); + } catch (e) { + return TimeOfDay.now(); } } } + + + diff --git a/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart b/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart index a48ca10..28a55ba 100644 --- a/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart +++ b/lib/presentation/emergency_services/call_ambulance/tracking_screen.dart @@ -119,7 +119,7 @@ class TrackingScreen extends StatelessWidget { backgroundColor: AppColors.lightRedButtonColor, borderColor: Colors.transparent, text: "Share Your Live Locatin on Whatsapp".needTranslation, - fontSize: 12.fSize, + fontSize: 12.f, textColor: AppColors.primaryRedColor, iconColor: AppColors.primaryRedColor, onPressed: () {}, @@ -170,7 +170,7 @@ class TrackingScreen extends StatelessWidget { return Row( spacing: 16.h, children: [ - Utils.buildImgWithNetwork(url: "", iconColor: Colors.transparent) + Utils.buildImgWithNetwork(url: "",) .circle(52.h), Expanded( child: Column( @@ -244,7 +244,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "Please wait for the call".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.textColor, ), @@ -252,7 +252,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "...".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.errorColor, ), @@ -265,7 +265,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "15:30".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.textColor, ), @@ -273,7 +273,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: " mins ".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.errorColor, ), @@ -281,7 +281,7 @@ class TrackingScreen extends StatelessWidget { TextSpan( text: "to hospital".needTranslation, style: TextStyle( - fontSize: 21.fSize, + fontSize: 21.f, fontWeight: FontWeight.w600, color: AppColors.textColor, ), diff --git a/lib/presentation/emergency_services/widgets/nearestERItem.dart b/lib/presentation/emergency_services/widgets/nearestERItem.dart index 3dc2aa1..6a8fdbc 100644 --- a/lib/presentation/emergency_services/widgets/nearestERItem.dart +++ b/lib/presentation/emergency_services/widgets/nearestERItem.dart @@ -49,7 +49,7 @@ class NearestERItem extends StatelessWidget { ).toShimmer2(isShow: isLoading) : Utils.buildImgWithNetwork( url: nearestERItem.projectImageURL ?? '', - iconColor: Colors.transparent, + // iconColor: Colors.transparent, ).circle(24.h).toShimmer2(isShow: isLoading), const SizedBox(width: 12), Expanded( diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 2f72227..0e9eb30 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -351,7 +351,16 @@ class _LandingPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Quick Links".needTranslation.toText16(isBold: true), + CustomButton(text: "Quick Links".needTranslation, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: ActiveMedicationPage(), + ), + ); + }, + ), + // "Quick Links".needTranslation.toText16(isBold: true), Row( children: [ "View medical file".needTranslation.toText12(color: AppColors.primaryRedColor), diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index ce2f87c..3aab1de 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -77,5 +77,7 @@ static const Color calenderTextColor = Color(0xFFD0D0D0); static const Color lightGreenButtonColor = Color(0x2618C273); static const Color lightRedButtonColor = Color(0x1AED1C2B); +static const Color lightGreyTextColor = Color(0xFF959595); +static const Color labelColorYellow = Color(0xFFFBCB6E); } From 77242825bbfd2653b310de8836409303d1fd91c3 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Tue, 18 Nov 2025 10:54:57 +0300 Subject: [PATCH 04/12] fix toggle --- .../active_prescriptions_view_model.dart | 168 +--- .../active_prescriptions_response_model.dart | 2 +- .../active_medication_page.dart | 730 ++++++++++++------ 3 files changed, 512 insertions(+), 388 deletions(-) diff --git a/lib/features/active_prescriptions/active_prescriptions_view_model.dart b/lib/features/active_prescriptions/active_prescriptions_view_model.dart index 03f84ea..e4da04d 100644 --- a/lib/features/active_prescriptions/active_prescriptions_view_model.dart +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -5,36 +5,24 @@ import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescri import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class ActivePrescriptionsViewModel extends ChangeNotifier { - bool isActivePrescriptionsDetailsLoading = false; - late ActivePrescriptionsRepo activePrescriptionsRepo; late ErrorHandlerService errorHandlerService; + List activePrescriptionsDetailsList = []; ActivePrescriptionsViewModel({ required this.activePrescriptionsRepo, required this.errorHandlerService, }); - List activePrescriptionsDetailsList = []; - - initActivePrescriptionsViewModel() { - getActiveMedications(); - notifyListeners(); - } - - setPrescriptionsDetailsLoading() { - isActivePrescriptionsDetailsLoading = true; - notifyListeners(); - } - - // Get medications list Future getActiveMedications({ Function(dynamic)? onSuccess, Function(String)? onError, }) async { - final result = await activePrescriptionsRepo.getActivePrescriptionsDetails(); + final result = + await activePrescriptionsRepo.getActivePrescriptionsDetails(); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async => + await errorHandlerService.handleError(failure: failure), (apiResponse) { if (apiResponse.messageStatus == 1) { activePrescriptionsDetailsList = apiResponse.data ?? []; @@ -56,140 +44,58 @@ class ActivePrescriptionsViewModel extends ChangeNotifier { return DateTime.tryParse(date) ?? DateTime.now(); } - // Extract numeric value ( "3 / week" → 3) - int extractNumberFromFrequency(String? frequency) { - if (frequency == null) return 1; - final m = RegExp(r'(\d+)').firstMatch(frequency); - if (m != null) return int.tryParse(m.group(1)!) ?? 1; - return 1; - } - - // Generate medication days based on frequency text List generateMedicationDays(ActivePrescriptionsResponseModel med) { final start = parseDate(med.startDate); final duration = med.days ?? 0; - final frequency = (med.frequency ?? "").toLowerCase().trim(); - - List result = []; - if (duration <= 0) return result; - - // Every N hours ( "Every Six Hours", "Every 8 hours") - if (frequency.contains("hour")) { - final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); - int intervalHours = 0; - - if (match != null) { - intervalHours = int.tryParse(match.group(1)!) ?? 0; - } else { - // handle text numbers like "Every six hours" - final textNum = { - "one": 1, - "two": 2, - "three": 3, - "four": 4, - "five": 5, - "six": 6, - "seven": 7, - "eight": 8, - "nine": 9, - "ten": 10, - "twelve": 12, - }; - for (var key in textNum.keys) { - if (frequency.contains(key)) { - intervalHours = textNum[key]!; - break; - } - } - } - - if (intervalHours > 0) { - for (int day = 0; day < duration; day++) { - final dayStart = start.add(Duration(days: day)); - for (int hour = 0; hour < 24; hour += intervalHours) { - result.add(DateTime(dayStart.year, dayStart.month, dayStart.day, hour)); - } - } - return result; - } - } - - // Daily (every day) - if (frequency.contains("day") && - !frequency.contains("every other") && - !frequency.contains("every ")) { - for (int i = 0; i < duration; i++) { - result.add(start.add(Duration(days: i))); - } - } - - // Every other day - else if (frequency.contains("every other day")) { - for (int i = 0; i < duration; i += 2) { - result.add(start.add(Duration(days: i))); - } + if (duration <= 0) return []; + final f = (med.frequency ?? "").toLowerCase().trim(); + int intervalDays = 1; + + if (f.contains("every six hours") || + f.contains("every 6 hours") || + f.contains("every four hours") || + f.contains("every 4 hours") || + f.contains("every eight hours") || + f.contains("every 8 hours") || + f.contains("every 12 hours") || + f.contains("every twelve hours") || + f.contains("every 24 hours") || + f.contains("3 times a day") || + f.contains("once a day")) { + intervalDays = 1; } - - // Every N days → e.g. "Every 3 days", "Every 5 days" - else if (frequency.contains("every") && frequency.contains("day")) { - final match = RegExp(r'every\s+(\d+)').firstMatch(frequency); - final interval = match != null ? int.tryParse(match.group(1)!) ?? 1 : 1; - for (int i = 0; i < duration; i += interval) { - result.add(start.add(Duration(days: i))); - } + else if (f.contains("once a week")) { + intervalDays = 7; } - - // Once or twice a week - else if (frequency.contains("once a week")) { - for (int i = 0; i < duration; i += 7) { - result.add(start.add(Duration(days: i))); - } - } else if (frequency.contains("twice a week")) { - for (int i = 0; i < duration; i += 3) { - result.add(start.add(Duration(days: i))); - } + else if (f.contains("every 3 days")) { + intervalDays = 3; } - - // Numeric frequency like "3 / week", "2 / week" - else if (frequency.contains("week")) { - int timesPerWeek = extractNumberFromFrequency(frequency); - double interval = 7 / timesPerWeek; - double dayPointer = 0; - - for (int i = 0; i < duration; i++) { - if (i >= dayPointer.floor()) { - result.add(start.add(Duration(days: i))); - dayPointer += interval; - } - } + else if (f.contains("every other day")) { + intervalDays = 2; } - else { - result.add(start); - } - - - final unique = {}; - for (final d in result) { - unique["${d.year}-${d.month}-${d.day}"] = d; + List result = []; + for (int offset = 0; offset < duration; offset += intervalDays) { + result.add(start.add(Duration(days: offset))); } - return unique.values.toList()..sort((a, b) => a.compareTo(b)); + return result; } - bool sameYMD(DateTime a, DateTime b) => a.year == b.year && a.month == b.month && a.day == b.day; - // Filter medications for selected day - List getMedsForSelectedDay(DateTime selectedDate) { - final target = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + List getMedsForSelectedDay( + DateTime selectedDate) { + final clean = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + return activePrescriptionsDetailsList.where((med) { final days = generateMedicationDays(med); - return days.any((d) => sameYMD(d, target)); + return days.any((d) => sameYMD(d, clean)); }).toList(); } } + diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart index eb216a6..42faafa 100644 --- a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -35,7 +35,7 @@ class ActivePrescriptionsResponseModel { int? scaleOffset; String? startDate; - // ✅ Added for reminder feature + // Added for reminder feature List selectedDoseTimes = []; bool isReminderOn = false; // toggle status diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index aa2abe5..d27b35d 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -18,6 +18,7 @@ import '../../widgets/buttons/custom_button.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; // for date formatting import 'package:provider/provider.dart'; import '../../widgets/loader/bottomsheet_loader.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class ActiveMedicationPage extends StatefulWidget { @@ -32,43 +33,98 @@ class _ActiveMedicationPageState extends State { late DateTime selectedDate; List selectedDayMeds = []; ActivePrescriptionsViewModel? activePreVM; - Map medReminderStatus = {}; + + + Map medReminderStatus = {}; + + String _buildMedKey(ActivePrescriptionsResponseModel med) { + return "${med.itemId}_${med.startDate}_${med.days}_${med.frequency}"; + } + + int _getDosesCount(ActivePrescriptionsResponseModel med) { + return med.frequencyNumber ?? 1; + } @override - void initState() { + void initState() { super.initState(); currentDate = DateTime.now(); selectedDate = currentDate; + WidgetsBinding.instance.addPostFrameCallback((_) async { - activePreVM = Provider.of(context, listen: false); + activePreVM = + Provider.of(context, listen: false); LoaderBottomSheet.showLoader(); await activePreVM!.getActiveMedications( - onSuccess: (_) { + onSuccess: (_) async { LoaderBottomSheet.hideLoader(); - final todayMeds = activePreVM!.getMedsForSelectedDay(selectedDate); - setState(() { - selectedDayMeds = todayMeds; + + final todayMeds = + activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() => selectedDayMeds = todayMeds); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await loadSavedReminders(); }); }, onError: (_) { LoaderBottomSheet.hideLoader(); }, ); - activePreVM!.addListener(() { + + activePreVM!.addListener(() { if (!mounted) return; - final medsForDay = activePreVM!.getMedsForSelectedDay(selectedDate); + final medsForDay = + activePreVM!.getMedsForSelectedDay(selectedDate); setState(() => selectedDayMeds = medsForDay); }); }); } + Future loadSavedReminders() async { + final prefs = await SharedPreferences.getInstance(); + + for (final med in activePreVM!.activePrescriptionsDetailsList) { + final medKey = _buildMedKey(med); + final doses = _getDosesCount(med); + + med.selectedDoseTimes = + List.filled(doses, null, growable: false); + + for (int i = 0; i < doses; i++) { + final saved = prefs.getString("doseTime_${medKey}_$i"); + if (saved != null) { + med.selectedDoseTimes[i] = saved; + } + } + + final reminderOn = + prefs.getBool("reminderStatus_$medKey") ?? false; + med.isReminderOn = reminderOn; + medReminderStatus[medKey] = reminderOn; + } + + setState(() {}); + } + + Future saveReminderStatus(String medKey, bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool("reminderStatus_$medKey", value); + } + + Future saveDoseTime( + String medKey, int doseIndex, String time) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString("doseTime_${medKey}_$doseIndex", time); + } + + List getUpcomingDays() => + List.generate(7, (index) => currentDate.add(Duration(days: index))); - List getUpcomingDays() => List.generate(7, (index) => currentDate.add(Duration(days: index))); @override Widget build(BuildContext context) { final days = getUpcomingDays(); - final dateText = "${selectedDate.day}${getSuffix(selectedDate.day)} ${DateFormat.MMMM().format(selectedDate)}"; - return Scaffold( + return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( onBackPressed: () => Navigator.of(context).pop(), @@ -99,13 +155,13 @@ class _ActiveMedicationPageState extends State { }, ), ), - SizedBox(height: 20.h), + SizedBox(height: 20.h), RichText( text: TextSpan( children: [ TextSpan( text: "${selectedDate.day}", - style: TextStyle( + style: TextStyle( color: AppColors.textColor, fontSize: 16, fontWeight: FontWeight.w500, @@ -115,7 +171,7 @@ class _ActiveMedicationPageState extends State { child: Transform.translate( offset: const Offset(0, -4), child: Text( - getSuffix(selectedDate.day), + _getSuffix(selectedDate.day), style: const TextStyle( fontSize: 12, color: AppColors.textColor, @@ -135,121 +191,163 @@ class _ActiveMedicationPageState extends State { ], ), ), - Text("Medications".needTranslation, + Text("Medications".needTranslation, style: TextStyle( color: AppColors.primaryRedBorderColor, fontSize: 12.f, fontWeight: FontWeight.w500)), - SizedBox(height: 16.h), + SizedBox(height: 16.h), Expanded( child: SingleChildScrollView( - child: selectedDayMeds.isNotEmpty - ? ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: selectedDayMeds.length, - itemBuilder: (context, index) { - final med = selectedDayMeds[index]; - final doses = med.doseDailyQuantity ?? 1; - med.selectedDoseTimes ??= List.filled(doses, null); - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - margin: EdgeInsets.all(10), - child: Column( - children: [ - _buildMedHeader(med), - Row( - crossAxisAlignment: CrossAxisAlignment.center, + child: selectedDayMeds.isNotEmpty + ? ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: selectedDayMeds.length, + itemBuilder: (context, index) { + final med = selectedDayMeds[index]; + final doses = _getDosesCount(med); + if (med.selectedDoseTimes.length != doses) { + final old = med.selectedDoseTimes; + med.selectedDoseTimes = + List.filled(doses, null, + growable: false); + for (int i = 0; + i < old.length && i < doses; + i++) { + med.selectedDoseTimes[i] = old[i]; + } + } + + return Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + margin: EdgeInsets.all(10), + child: Column( + children: [ + _buildMedHeader(med), + Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + // Utils.buildSvgWithAssets( + // icon: AppAssets., + // height: 18.h, + // width: 18.h, + // iconColor: + // AppColors.lightGreyTextColor, + // ), + Icon( + Icons.info_outline, + color: AppColors + .lightGreyTextColor, + size: 18, + ), + SizedBox(width: 6.h), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Remarks: " + .needTranslation, + style: TextStyle( + color: + AppColors.textColor, + fontWeight: + FontWeight.w600, + fontSize: 10, + ), + ), + TextSpan( + text: + "some remarks about the prescription will be here" + .needTranslation, + style: TextStyle( + color: AppColors + .lightGreyTextColor, + fontWeight: + FontWeight.normal, + fontSize: 10, + ), + ), + ], + ), + ), + ), + ], + ).paddingOnly(left: 16, right: 16), + const Divider( + color: AppColors.greyColor), + GestureDetector( + onTap: () => showDoseDialog(med), + child: Row( children: [ - Icon( - Icons.info_outline, - color: AppColors.lightGreyTextColor, - size: 20, + Container( + width: 40.h, + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: + BorderRadius.circular(10), + ), + child: + Utils.buildSvgWithAssets( + icon: AppAssets.bell, + height: 24.h, + width: 24.h, + iconColor: + AppColors.greyTextColor, + ), ), - SizedBox(width: 6.h), + SizedBox(width: 12.h), Expanded( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: "Remarks: ".needTranslation, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Set Reminder" + .needTranslation, style: TextStyle( - color: AppColors.textColor, - fontWeight: FontWeight.w600, - fontSize: 10, - ), - ), - TextSpan( - text: "some remarks about the prescription will be here".needTranslation, + fontSize: 14.f, + fontWeight: + FontWeight.w600, + color: AppColors + .textColor)), + Text( + "Notify me before the consumption time" + .needTranslation, style: TextStyle( - color: AppColors.lightGreyTextColor, - fontWeight: FontWeight.normal, - fontSize: 10, - ), - ), - ], - ), + fontSize: 12.f, + color: AppColors + .textColorLight, + )), + ], ), ), + _buildToggle(med), ], - ).paddingOnly(left: 16, right: 16), - const Divider(color: AppColors.greyColor), - // Reminder Section - GestureDetector( - onTap: () => showDoseDialog(med, index), - child: Row( - children: [ - Container( - width: 40.h, - height: 40.h, - alignment: Alignment.center, - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(10), - ), - child: Utils.buildSvgWithAssets( - icon: AppAssets.bell, - height: 24.h, - width: 24.h, - iconColor: AppColors.greyTextColor, - ), - ), - SizedBox(width: 12.h), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text("Set Reminder".needTranslation, - style: TextStyle( - fontSize: 14.f, - fontWeight: FontWeight.w600, - color: AppColors.textColor)), - Text("Notify me before the consumption time".needTranslation, - style: TextStyle( - fontSize: 12.f, - color: AppColors.textColorLight, - )), - ], - ), - ), - _buildToggle(index) - ], - ).paddingAll(16), - ), - const Divider(color: AppColors.greyColor), - _buildButtons(), - ], - ), - ); - }, - ) - : Utils.getNoDataWidget(context, - noDataText: "No medications today".needTranslation), + ).paddingAll(16), + ), + const Divider( + color: AppColors.greyColor), + _buildButtons(), + ], + ), + ); + }, + ) + : Utils.getNoDataWidget( + context, + noDataText: + "No medications today".needTranslation, + ), ), ), ], @@ -257,55 +355,67 @@ class _ActiveMedicationPageState extends State { ); } - //medicine card - Widget _buildMedHeader(ActivePrescriptionsResponseModel med) => Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - width: 59.h, - height: 59.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.spacerLineColor, - borderRadius: 30.r, - hasShadow: false, + // medicine card + Widget _buildMedHeader(ActivePrescriptionsResponseModel med) => + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: 59.h, + height: 59.h, + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.spacerLineColor, + borderRadius: 30.r, + hasShadow: false, + ), + child: Utils.buildImgWithNetwork( + url: med.productImageString ?? "", + iconColor: Colors.transparent) + .circle(52.h)), + ), + SizedBox(width: 12.h), + Expanded( + child: Text( + med.itemDescription ?? "", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor), ), - child: Utils.buildImgWithNetwork( - url: med.productImageString ?? "" ).circle(52.h) - ), - ), - SizedBox(width: 12.h), - Expanded( - child: Text( - med.itemDescription ?? "", - style: TextStyle( - fontSize: 16.f, - fontWeight: FontWeight.w600, - color: AppColors.textColor), + ), + ]), + SizedBox(height: 12.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget( + labelText: + "Route: ${med.route}".needTranslation), + AppCustomChipWidget( + labelText: + "Frequency: ${med.frequency}".needTranslation), + AppCustomChipWidget( + labelText: + "Daily Dose: ${med.doseDailyQuantity}" + .needTranslation), + AppCustomChipWidget( + labelText: + "Duration: ${med.days}".needTranslation), + ], ), - ), - ]), - SizedBox(height: 12.h), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - AppCustomChipWidget(labelText: "Route: ${med.route}".needTranslation), - AppCustomChipWidget(labelText: "Frequency: ${med.frequency}".needTranslation), - AppCustomChipWidget(labelText: "Daily Dose: ${med.doseDailyQuantity}".needTranslation), - AppCustomChipWidget(labelText: "Duration: ${med.days}".needTranslation), ], ), - ], - ), - ); + ); Widget _buildButtons() => Padding( - padding: EdgeInsets.all(16), + padding: EdgeInsets.all(16), child: Row(children: [ Expanded( child: CustomButton( @@ -317,24 +427,28 @@ class _ActiveMedicationPageState extends State { textColor: AppColors.errorColor, ), ), - SizedBox(width: 12.h), + SizedBox(width: 12.h), Expanded( child: CustomButton( - text: "Read Instructions".needTranslation, fontSize: 13.f, onPressed: () {})), + text: "Read Instructions".needTranslation, + fontSize: 13.f, + onPressed: () {})), ]), ); - Widget _buildToggle(int index) { - final value = medReminderStatus[index] ?? false; + Widget _buildToggle(ActivePrescriptionsResponseModel med) { + final medKey = _buildMedKey(med); + final value = medReminderStatus[medKey] ?? false; return GestureDetector( onTap: () async { - await showDoseDialog(selectedDayMeds[index], index); - setState(() { - if ((selectedDayMeds[index].selectedDoseTimes ?? []).any((t) => t != null)) { - medReminderStatus[index] = true; - } - }); + await showDoseDialog(med); + final hasTime = + (med.selectedDoseTimes).any((t) => t != null); + medReminderStatus[medKey] = hasTime; + await saveReminderStatus(medKey, hasTime); + + setState(() {}); }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -342,11 +456,14 @@ class _ActiveMedicationPageState extends State { height: 28.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: value ? AppColors.lightGreenColor : AppColors.greyColor.withOpacity(0.3), + color: value + ? AppColors.lightGreenColor + : AppColors.greyColor.withOpacity(0.3), ), child: AnimatedAlign( duration: const Duration(milliseconds: 200), - alignment: value ? Alignment.centerRight : Alignment.centerLeft, + alignment: + value ? Alignment.centerRight : Alignment.centerLeft, child: Padding( padding: const EdgeInsets.all(3), child: Container( @@ -354,7 +471,9 @@ class _ActiveMedicationPageState extends State { height: 22.h, decoration: BoxDecoration( shape: BoxShape.circle, - color: value ? AppColors.textGreenColor : AppColors.greyTextColor, + color: value + ? AppColors.textGreenColor + : AppColors.greyTextColor, ), ), ), @@ -363,10 +482,16 @@ class _ActiveMedicationPageState extends State { ); } - Future showDoseDialog(ActivePrescriptionsResponseModel med, int medIndex) { - final doses = med.frequencyNumber ?? 1; + + Future showDoseDialog(ActivePrescriptionsResponseModel med) { + final doses = _getDosesCount(med); if (med.selectedDoseTimes.length != doses) { - med.selectedDoseTimes = List.generate(doses, (_) => null); + final old = med.selectedDoseTimes; + med.selectedDoseTimes = + List.filled(doses, null, growable: false); + for (int i = 0; i < old.length && i < doses; i++) { + med.selectedDoseTimes[i] = old[i]; + } } return showModalBottomSheet( @@ -378,19 +503,22 @@ class _ActiveMedicationPageState extends State { height: 520.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bottomSheetBgColor, - customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + customBorder: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), hasShadow: true, ), - child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - Text( + Text( "Reminders".needTranslation, style: TextStyle( fontSize: 20.f, @@ -400,11 +528,17 @@ class _ActiveMedicationPageState extends State { ), GestureDetector( onTap: () => Navigator.pop(context), - child: Icon(Icons.close, color:AppColors.blackBgColor), + child: Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + height: 24.h, + width: 24.h, + iconColor: + AppColors.blackBgColor, + ), ), ], ), - SizedBox(height: 20.h), + SizedBox(height: 20.h), Expanded( child: ListView.builder( itemCount: doses, @@ -415,64 +549,91 @@ class _ActiveMedicationPageState extends State { AppColors.labelColorYellow, AppColors.purpleBg ][doseIndex % 4]; - - final doseLabel = "${doseIndex + 1}${getSuffix(doseIndex + 1)}"; - final time = med.selectedDoseTimes[doseIndex] ?? "Not set yet"; - + final doseLabel = + "${doseIndex + 1}${_getSuffix(doseIndex + 1)}"; + final time = + med.selectedDoseTimes[doseIndex] ?? + "Not set yet"; return GestureDetector( onTap: () { Navigator.pop(context); - showTimePickerSheet(med, medIndex, doseIndex); + showTimePickerSheet(med, doseIndex); }, child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 16.r, hasShadow: false, ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Container( - padding: const EdgeInsets.symmetric( - vertical: 6, horizontal: 14), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 14), decoration: BoxDecoration( color: badgeColor, borderRadius: BorderRadius.circular(12), ), - child: Text( - doseLabel, - style: TextStyle( - color: AppColors.whiteColor, - fontWeight: FontWeight.bold, - fontSize: 16.f, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "${doseIndex + 1}", + style: TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -4), + child: Text( + _getSuffix(doseIndex + 1), + style: TextStyle( + color: AppColors.whiteColor, + fontSize: 10.f, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], ), ), ), SizedBox(height: 8.h), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( "Set reminder for $doseLabel dose", - style: TextStyle( + style: TextStyle( color: AppColors.textColor, fontWeight: FontWeight.bold, fontSize: 16.f, ), ), ), - Icon(Icons.arrow_forward_outlined, - size: 24.w, color: AppColors.textColor), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + height: 24.h, + width: 24.h, + iconColor: + AppColors.textColor, + ), ], ), - SizedBox(height: 4.h), + SizedBox(height: 4.h), Text( time, - style: TextStyle( + style: TextStyle( fontSize: 12.f, color: AppColors.greyTextColor, fontWeight: FontWeight.w500, @@ -494,7 +655,7 @@ class _ActiveMedicationPageState extends State { void showTimePickerSheet( - ActivePrescriptionsResponseModel med, int medIndex, int doseIndex) { + ActivePrescriptionsResponseModel med, int doseIndex) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -502,20 +663,24 @@ class _ActiveMedicationPageState extends State { builder: (_) => Container( width: double.infinity, height: 460.h, - decoration: BoxDecoration( + decoration: const BoxDecoration( color: AppColors.bottomSheetBgColor, - borderRadius: - BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), ), child: ReminderTimerDialog( med: med, - frequencyNumber: med.doseDailyQuantity ?? 1, + frequencyNumber: _getDosesCount(med), doseIndex: doseIndex, - onTimeSelected: (String time) { - setState(() { - med.selectedDoseTimes[doseIndex] = time; - medReminderStatus[medIndex] = true; - }); + onTimeSelected: (String time) async { + final medKey = _buildMedKey(med); + med.selectedDoseTimes[doseIndex] = time; + await saveDoseTime(medKey, doseIndex, time); + medReminderStatus[medKey] = true; + await saveReminderStatus(medKey, true); + setState(() {}); }, ), ), @@ -529,7 +694,8 @@ class _ActiveMedicationPageState extends State { return GestureDetector( onTap: () { final vm = - Provider.of(context, listen: false); + Provider.of(context, + listen: false); setState(() { selectedDate = date; selectedDayMeds = vm.getMedsForSelectedDay(date); @@ -544,10 +710,11 @@ class _ActiveMedicationPageState extends State { ? AppColors.secondaryLightRedBorderColor : Colors.transparent, border: Border.all( - color: isSelected - ? AppColors.primaryRedBorderColor - : AppColors.spacerLineColor, - width: 1), + color: isSelected + ? AppColors.primaryRedBorderColor + : AppColors.spacerLineColor, + width: 1, + ), ), child: Padding( padding: const EdgeInsets.all(8.0), @@ -563,7 +730,7 @@ class _ActiveMedicationPageState extends State { fontSize: 11.f, fontWeight: FontWeight.w500), ), - SizedBox(height: 5.h), + SizedBox(height: 5.h), Text("${date.day}", style: TextStyle( fontSize: 16.f, @@ -577,14 +744,14 @@ class _ActiveMedicationPageState extends State { ); } - String getSuffix(int day) { + String _getSuffix(int day) { if (day == 1 || day == 21 || day == 31) return "st"; if (day == 2 || day == 22) return "nd"; if (day == 3 || day == 23) return "rd"; return "th"; } -} +} class ReminderTimerDialog extends StatefulWidget { @@ -617,32 +784,94 @@ class _ReminderTimerDialogState extends State { ["06:00 PM", "07:00 PM", "08:00 PM", "09:00 PM"], // Evening ]; + String _getSuffix(int number) { + if (number == 1 || number == 21 || number == 31) return "st"; + if (number == 2 || number == 22) return "nd"; + if (number == 3 || number == 23) return "rd"; + return "th"; + } + @override Widget build(BuildContext context) { final int bucket = widget.doseIndex.clamp(0, 2); final List times = presetTimes[bucket]; + return Padding( padding: const EdgeInsets.all(16), child: Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.bottomSheetBgColor, - customBorder: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + customBorder: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), hasShadow: true, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Time for ${widget.doseIndex + 1} dose".needTranslation, - style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.bold), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "Time for ", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + TextSpan( + text: "${widget.doseIndex + 1}", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + WidgetSpan( + child: Transform.translate( + offset: const Offset(0, -6), + child: Text( + _getSuffix(widget.doseIndex + 1), + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + ), + ), + TextSpan( + text: " reminder", + style: TextStyle( + fontSize: 18.f, + fontWeight: FontWeight.bold, + color: AppColors.textColor, + ), + ), + ], + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child:Utils.buildSvgWithAssets( + icon: AppAssets.close_bottom_sheet_icon, + height: 24.h, + width: 24.h, + iconColor: + AppColors.blackBgColor, + ),), + ], ), - SizedBox(height: 12.h), - // Preset times + SizedBox(height: 12.h), Wrap( spacing: 8, runSpacing: 8, - alignment: WrapAlignment.start, children: times.map((t) { bool selected = _selectedTime == t; return AppCustomChipWidget( @@ -660,7 +889,7 @@ class _ReminderTimerDialogState extends State { ), borderRadius: BorderRadius.circular(12), ), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 14), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), onChipTap: () { setState(() { _selectedTime = t; @@ -672,27 +901,25 @@ class _ReminderTimerDialogState extends State { ); }).toList(), ), - SizedBox(height: 25.h), + SizedBox(height: 25.h), GestureDetector( onTap: () { - setState(() { - showPicker = !showPicker; - }); + setState(() => showPicker = !showPicker); }, child: Center( child: Column( children: [ Text( bigTimeText, - style: TextStyle( + style: TextStyle( fontSize: 48.f, fontWeight: FontWeight.bold, - color: AppColors.textColor + color: AppColors.textColor, ), ), Text( selectedTime.period == DayPeriod.am ? "AM" : "PM", - style: TextStyle( + style: TextStyle( fontSize: 20.f, fontWeight: FontWeight.bold, color: AppColors.greyTextColor, @@ -702,8 +929,7 @@ class _ReminderTimerDialogState extends State { ), ), ), - SizedBox(height: 15.h), - // Time picker + SizedBox(height: 15.h), if (showPicker) SizedBox( height: 100.h, @@ -717,20 +943,16 @@ class _ReminderTimerDialogState extends State { selectedTime.hour, selectedTime.minute, ), - onDateTimeChanged: (DateTime newTime) { + onDateTimeChanged: (newTime) { setState(() { _selectedTime = null; - selectedTime = TimeOfDay( - hour: newTime.hour, - minute: newTime.minute, - ); - bigTimeText = - selectedTime.format(context).split(" ")[0]; + selectedTime = TimeOfDay(hour: newTime.hour, minute: newTime.minute); + bigTimeText = selectedTime.format(context).split(" ")[0]; }); }, ), ), - SizedBox(height: 25.h), + SizedBox(height: 25.h), Row( children: [ Expanded( @@ -745,8 +967,7 @@ class _ReminderTimerDialogState extends State { ), ), onPressed: () async { - final selectedFormattedTime = - selectedTime.format(context); + final selectedFormattedTime = selectedTime.format(context); widget.onTimeSelected(selectedFormattedTime); try { final parts = selectedFormattedTime.split(":"); @@ -756,7 +977,6 @@ class _ReminderTimerDialogState extends State { if (isPM && hour != 12) hour += 12; if (!isPM && hour == 12) hour = 0; int totalMinutes = hour * 60 + minute; - // Call setCalender() await setCalender( context, eventId: widget.med.itemId.toString(), @@ -768,21 +988,18 @@ class _ReminderTimerDialogState extends State { route: widget.med.route ?? "", ); ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Reminder added to calendar ✅".needTranslation)), + SnackBar(content: Text("Reminder added to calendar ✅".needTranslation)), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text("Error while setting calendar: $e".needTranslation)), + SnackBar(content: Text("Error while setting calendar: $e".needTranslation)), ); } Navigator.pop(context); }, child: Text( LocaleKeys.save.tr(), - style: TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.f, ), @@ -797,7 +1014,6 @@ class _ReminderTimerDialogState extends State { ); } - TimeOfDay _parseTime(String t) { try { int hour = int.parse(t.split(":")[0]); @@ -806,7 +1022,7 @@ class _ReminderTimerDialogState extends State { if (pm && hour != 12) hour += 12; if (!pm && hour == 12) hour = 0; return TimeOfDay(hour: hour, minute: minute); - } catch (e) { + } catch (_) { return TimeOfDay.now(); } } @@ -815,3 +1031,5 @@ class _ReminderTimerDialogState extends State { + + From e17d3cde87a0fe0a2060b8c98531ca0c8eff074b Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Tue, 18 Nov 2025 11:19:15 +0300 Subject: [PATCH 05/12] fix toggle --- lib/presentation/emergency_services/widgets/nearestERItem.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/emergency_services/widgets/nearestERItem.dart b/lib/presentation/emergency_services/widgets/nearestERItem.dart index 6a8fdbc..3dc2aa1 100644 --- a/lib/presentation/emergency_services/widgets/nearestERItem.dart +++ b/lib/presentation/emergency_services/widgets/nearestERItem.dart @@ -49,7 +49,7 @@ class NearestERItem extends StatelessWidget { ).toShimmer2(isShow: isLoading) : Utils.buildImgWithNetwork( url: nearestERItem.projectImageURL ?? '', - // iconColor: Colors.transparent, + iconColor: Colors.transparent, ).circle(24.h).toShimmer2(isShow: isLoading), const SizedBox(width: 12), Expanded( From 0b3fea230f75bcda90ded25e29d3a93cf9adf610 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Mon, 15 Dec 2025 10:28:48 +0300 Subject: [PATCH 06/12] monthly report --- assets/images/jpg/report.jpg | Bin 0 -> 37509 bytes lib/core/api_consts.dart | 1 + lib/core/app_assets.dart | 4 + lib/core/dependencies.dart | 6 + .../active_prescriptions_response_model.dart | 205 ++++--------- .../hmg_services_component_model.dart | 2 + .../terms_conditions_repo.dart | 60 ++++ .../terms_conditions_view_model.dart | 45 +++ lib/main.dart | 6 +- .../active_medication_page.dart | 2 +- .../hmg_services/services_page.dart | 25 +- .../hmg_services/services_view.dart | 21 +- .../monthly_reports/monthly_reports_page.dart | 283 ++++++++++++++++++ .../monthly_reports/user_agreement_page.dart | 117 ++++++++ lib/routes/app_routes.dart | 7 +- pubspec.yaml | 2 +- 16 files changed, 632 insertions(+), 154 deletions(-) create mode 100644 assets/images/jpg/report.jpg create mode 100644 lib/features/terms_conditions/terms_conditions_repo.dart create mode 100644 lib/features/terms_conditions/terms_conditions_view_model.dart create mode 100644 lib/presentation/monthly_reports/monthly_reports_page.dart create mode 100644 lib/presentation/monthly_reports/user_agreement_page.dart diff --git a/assets/images/jpg/report.jpg b/assets/images/jpg/report.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5846cd538dec27f12011b3de3ecfbdae4d411f29 GIT binary patch literal 37509 zcmdpdWmH^EwrJx{Xj}s{(6}_N!QI^hbT{tq3GVJ5f+e_X2*I_H;GWgVj{Z0%)5 z=jZI?;wkDU0s0fU=wtnlW^NGOpIy8hB|y@DP^B|c*P@ek^{}QB;1cA7@dygh35jq) z1cZe6g*fPVc_0GZJbc`IJe<6IqWnCf{JeC3FQ7+i9&j5`9XW-+(|UZ80R3GkUteD? zUp_8Z4_j`Ch=>R`4=*<_FXv+qPEUUqFDpMz7f*)2FvwYZ!aVHVyzE_F=>EWH1$FiI zk^nt&`Zp1r-Ts00-|Fh0I;yMx&t09J|4_i6Xg$4jtpAJOe~Z{t&)?0OTgTed)!PGR z{n(x14`4S@Sr2O~FINveS68RMw5Vn0>gDQb=jui$EBmL`=vWNxUEr?1o~(busH=-A zyLft8xxlQI>uXf?Dj}m&f3G?#~QBS;p$BHr`V$Q|B)A2`TvOT@38Ry z$cwDPe}v_JWQO~X!TqPf{;TQHg8r!f!*m}X{$c#qE{_)P@o3r)X8`;^4Hf|Jef)%w z9@YR60HptYLH@6T@{jU&aAYK;$G4Baf4unLRvvl*Kun}oBy(K9oMb#l^zJK*7X$tRtbK z0f2-===8)G5Re{;blR+LvscEvfPN;aHZOx``drI`fmP~9ovcMna`#XfK1LrhrdGk% zjeQU-(2{=HQu!f;HY}k>=8yr8JRl=uprfJv!4(n^nVt{@6(X%|MdS`b3*}9f z`Su72B_{c(;}OQdw{ipNiwr~o0we*yfAgbA{a;_`%n~F6Ik&7x zOen+q$OCb{%YI1;!f#x`A^qKv=sC594v=Uu8u41z$hqW5OTN>TS?2XwEY9 z-X2D0Jt6Rt3@J{4f_xKEUn>qi%^rQOAwYUsw^M_rtlzC+kYU(rQ^c~OEPRwT=Y!0IL^O)r8V7$Yfys-95cw{p6!uYx0mr_qScft;5|{M zD5;D#n&b(wl(JyG>E~C_Oi+up&5Vw*ajhyNLKyC1ZnNqSyGB*m;_(Nz7n9-2*3xl8knkM(k+PH z6Nu&dleVY@PO86pSmI&Qa?yBHSLkLw>d{rs%R)%z+xQ<3`W2 z9_^mPvaTXm>bEIG7#sVOh!VLJFRP%W{?c($)d*48Z9b$tK{i?2JM^BD&Mc2pHy}?e zL835AHL*x{Yq|;8%~9+KL8}-UHPN|;+#WV8%uSg9v8&a@u&z5))lESNdj^OYX=)+ z6Xylqw;fzscPT5Zv7xMZs8ncmsTlBRP779-+K;1K+=Jr|#)kNy!V^bFmCf8sUSeJs zLeB4tV?S(OtT`i9Z+G)(u=~7`aH}gk3C>!k_F=+t4iG-6 zpqm*4zrA#}7_rWM(lQ{FUu;YSq^88=WWQqe@kpz=QJgnh6{`K5^q$asy%lX9&CL2t zYF*z}Fm8QyiwFHMHr`BYP%R&DqoSA{~T!H0x;&z^c(7!KY$>ztE!Tdih>fvp%}lPafKrGBwd zngFPQW>jwQhI^sK<|X`{(nZZr!m4Sv&NlxonsTY+=VbI8!UL~;h%4)S$K(?2IvaeotqUp@Mx%9-t(UV;J|k>DUb z>M^rQq*Aj$b# zq32hblU8!d?Fnr$If=-eVu{?c)fZm(iP(Ruc?Tu}&iLQs*N-in@uPIiUWtBSHtd+e z-%*`8`dRpVM@n{7yL_En)3bU-1MvVj`*K70@|QXiJIz%vZS2B#zzosfoW1Vm4_%DA z;|D;4kd#BP+*vVxc^$%kH#>27i-Fa~@nl?$@e_RgEy7D*$cFoQaixv{zFygz%6{h^1icY!9KXf7 z47_%QH|ax`-%^b2l6O(RBVSmda1LtaOScY)(0zg;kl`*xj5J%Xe=T-mG-6PUnd;F{Q2dZ}oMv&P&TMwd$juDyr{Qr0zL;cwL8`Huq7d zqC=a&=L-qma_xCLoiSCHe4kn`I-Zg!lL3^!WKQQrv?mxNbfyykSs25}W|qxqJcOV8 zVsmqn#%3za0W*02a*^DzkB>0pwuf&<;`)okacaOtrYhz>$ve`TNx&(Vv&R?gw8{6WhChjXvhGFpGW*NpL@N8ml)yP)i6+drHj-Y16n ztrY4c9Q4ZKX}R1oIizf3n*8tzbltV z;h{k4DZ=F?Y`X$!mQrb^o@Waa9YG86aV@bh4|!uUGHeg~OiO-lJMvh}u-w?wi#spY z;G`eVk_V52FzvANkcughidnw~vxfh?egG^}6|?dFG9<6Lre@UoD(_OoNrk+f?Lc~e zF8dpYmL2>lZ%%yaL>fm;*=O1&B8}zxH^FkCe8y}YHhZUIT;BJpL@`4)zNfwnc}y9y z_S1xIq01xFn?5tjSpk=50#p66%no|_xE>BWn~UHfzh;yq`{9Lf2K!d>RxaZMYDFix z!BM66i_{^5x8myA9|R1k97?!^A%peCX4QH`zTPXMlS(Vw!l4pnenIcMu(){qUr*BS z3DBxcvU8ONyz?_^*{B=PosD<=nb?4g_kF~b|8YiKx74}(536PjU4R|sGV)M^5_vN@ z507vv9fT#7p1!bxxd|pERcS~Yqr;+AL3~WXAHuejTG#wae5?Ycw+TM=k?EP&jY3Fm zaKba*(>0Q+Me)uo8nV~aDIH2<@W66ir<+$S&u{pgOJmw^Y=be~)2DRI6Kn*vy3_c4 z&Y%UV9^?^e44L+VGJae6Kmu}qC=?2l$;#%Ro>EjEHVH_TE#Bf+$a+ul8>{HT+E zT08M=Pe$MGfCs<{-VA|9`D|ax?mh=>E)@Zzij6es#PseqHRT!aC;Yr+`usOJ%W}*( zI@s23CWoBtV=#Z&q-;5t>SSF*ykJcSx3OSmSgXCw7qVqtBRRD8-R}ocAi-t2hNlIC zlWuiaO`j7C!>lKs1mIc2*cP~ ztwVX-X{}AI+XsAf-gSN%6{ym_FX02gfgg-M6jz;45sM3B=o;a4gV<*gA$ZvdmjxTs zCP=3~QM4%~&T2F9iP`D&JyFlZIc}j&-~J3eh*6Cw!?903NjTZ! z5l2B`rlEjn#eTlb`;__6SX{X=Ts6e$Ylhrcz}Z4hiqcH0) z%0|#k$CME5>>YAcX;)rddQ`_kobaaO6^`11a3`aNdxKL2gKM-~h+QZDbAehWPniftp+aG+W$sO|e~s@T}d zWRrg_J^ZQB-WwH?`=`jrMYpp#lG7HyLc35brmyxjSu+Bw`6!M~iZJFySUO3uXNe7s zEl+iH7L@{rxL7QB!bnHME=&DpQXEdc-3#6uxV&RX#woZ*46jLVM#E?VpQH{+GRwg& z+06q?LM%1;;^?M~nfe-}J*JYnh1H%5z9Z->ci4+3e|M)Ae?IwDYFUcwI-u1#tkDGm zB^xJ*P3&F?gKpUn^vX@>>SOOwI_~W=?S>(oL&SL`i>J+;ER(CqcEcYAtwnD-dqjl z96vd?{CFD|-H2-GOg-SdwlI!?o)~u?BU_X2Am7iIC@(D)02?hV|Hcd8A(3v|40-5r zI_L6dqZloqUaYsNjg={NDxVvNU<$? z)h{Y==tTHx4Aw=&z+Fy(Rf}Zt>fR~Gpmt$GDn;V3+gEghK6W}gf37zczZ3l1^@h1c zTB|0g7Jg9X9m+SPHT_qDe7YQGpk&sZQ1d(8Pk}__ba$WCHbxi63iqk*d z0L5J;5TSjdmcq}Kn9iL*f+GABn{1?3`2X!l|Fr_3e5JkLM8351WqaZI-brL$r8$u* z)=ZeY`b3GRLlt?~^^#abj*`MjF3VG7f;Z!Sp$VmF@57r&56$K3EH_NX8qTN4$}W}E z9d7Vh4ZAwbDCDpZXJW%)+SU;^%7r3n1(l}kvd2gdfCMbl*X~Aem_1+}WaCrV!rBb$ zNiIDX{e(=3Z1cC^FYuF!JI(h4HlG-8m>UVO^p>D& z*qP^T>Q!4Z(~L;tFD3Pn7CkRrbt%5g4wc7_HLnAJoL+@{PTWh?Y@K zsyAZM7DbCt9b zy{PAj*E=x?IND&rra*t)i;qpVF4M{v<(Ff|2jnq(47Z7=9I}?tQ6ZM2=n=KAhk49! zu};zP@6|M&G$MKb9tQs{82+bwlu-2qlG?MA#pPg!qt+%W=gh&xbZw^`cBBak;ndi5 zsO?xW>rN%R5VtIazPq*1ap#V|OALVRv?Z!apM4vIzCd5Jyc#Hxh=jtL(z|bdq!#Jm zPx(3}?-ut2wfgA-^aTFVZW%(DMNPLv2xffdbJ=zdB4bNHr(E(eg)HB98pxudUR5pZ zsC1wkw9$(6mvi}q#sW`oYGYi*-5yCkD=91$f5NxTBoK?%l4dV7|w_gy&+1 zKO~82ztI#T<~Vt+P0SF25FLy5~mzjQ>G`g$*t713_R?&74@ z=ZdOLkD3?E5IV9^8&Ie~pHYseN@;iP0#9ZX2Kttib!km35sZ;`d`@@(P};`X!0Abh z)$p-5rPf5S`E!%RR&bP~YHAp`=l%9AtKqfj&MuKn84bjB05U%N*mZu?byxYV#%Ag9 zc&88Ym^H?N8!f&rhIJii`fWS*`IQxN$_}|(#`x!JOpF9hG0uJrWp4(|mGy^uowJsW z8gNp4Fe{9~TV~ThpR74i;`D##mLGjJieBTaUftd_XTapKLJ!smT}`)Pf|IIxVr~B> z1+RN-#OPTGW@ypVQ9}F<4q6TRW?%B#UFr(zp?nI{T#)K3ZtD`P;HbE`41} zcjpx(wdjYhIrt!%n-tIIABW9B76->6X1E$RPRhlc^ubn(Nfm zw?)hc_*@q%hs~0iaFbxq{gMgzDNt4y>R*Zi>VPA@-ArwvHMtYJ<%oH)pvco3tfVeC zLie&;X3R!bTJ4h>Zl8Ya1Zu+N6g~neiJSkY#TapCQYJ@_buM^~zWPXMBoc3&z2~A% zbWhc=pW!z>e*x5yoPK%R#y55qHQeyYlO}I0_BScj8$^cGd6~;Ui}5LcIb4T*($d=U z6q{0*$goI<$9;!_Ri`X-q_ak%Z)3hh2Bjtvh$eSx`{PC`h%w^uH0R?)P@v!5u{=eJ z!OVF8*x7)*2sEd?BobOr7Je+*&o*Zky{hM8i`Xw>r~GdHgLI#qdWr?)%$HbE8@rSE zIYEgM^sc1Y82gqaZOkw&uDUiZ!wm64df{AkgdMy?MH1uE@Hv+UBHqc>54yY}kZ<1l zZQx=q{z9WoLXGV_ew2QNd~Az#!A5F_LZ7$)?NM8{haOy)uz_zC+bReNZIP@rnWA^U zzeYeGy)~eIY?pJXoO8VJxKY0;u9}$eE~xSBn~dsK+E(ri6GpL}_P?_u=2BsFZK^OF}JQ~|rA5apfO6q9sBtjof z$fje86f*KktFCtJ>q3bEi;N*vbIiV)7@eAGK(N4kZF_=?Ew{mshC8fwmAIs z_}L*#!azcn6+18`37!LW%DFdY?+~voNTW3|6=5yQw=*||B}X7YPK(A(g#E%Ip{zTf zx3*6|$z*r@_my5YGq^e@UpyaAh1P*aU}gfn$5}9JG%HomP5H(Z2qA*>giIgn1qf=M z)Fe-~pe(++1!FTvMQlt4x3TB5w_M?LGNH=1qgDIG3f7wjw8b1&8%1J@k_Lg zyE$E%)Ttc5V`ErEUvGdYP>3Zl3hbNn6H?5`blCLT+n23hGd{G^Qb4|62d%$1jc^N+ zQ~0I{=8Tjr1%v6O?2E+fd=7yxLmR@Z;ugi_A=C=}p)of$P=Oin{8ktG)C zI!KOzsS{HXS8Z@=-<|eLP9oALDerJ(5E3LE|0yIamTo(7BnImHl;t$>XBSFah#fAH zO|$X1c?6`p!{I`#*d9>QR%nH(IqC>;o<$102nG z0yRMMWX`M-V_-pooc~AQBl4zb2p)c*dsz|pxJ>mb>FB9sj3T@rC%C&&A`kgZ_4(BM zD51G}L600$eb4WisJNZk=bZ5neQJem$Se>@&_@#oNhIRZ5a;j$Vg& z7H^u;Y{Ib<@Twx2`1;v(>M+oBBrzW#eBIwCE_2C@DyIW=HrISUsZl;?)6m)^$fuC! zSFQicU@gZiir~Uyq-0SAnJoNW5;INaha7P;aWGyIIk|!TjgvPTaX7E*(fDGzWBF2C zu+iCalX}t{`Jm#x#hikm<>n__4x7AVZ>2Z7?^tI@4Isbts_3B}=4$VR4Cigr8S+=q zz}oYfCtD8nG%-0J6y6uuH+A#TqByW?5|8nq7DKq}L~2K_@Ct^XO-VfMDl-Jj@YuE`mJKzeL$PNDh7jSKE8 zu12N?G8+Z6p)lOtcNsw*kr4{vL?qk@^K|Lex>tne{!12|mGCYWl5)O!33-*e&SZS6JxX*NT;EhxDb; zS%wlI2*_M5Df2Exf~+T2G~ewal68#-3`cf2%``ac;=Z5cCoxX?Ae_Lp+B?Shy^B(* zn0*AHalZq_QpyXeRe=j6+15Ujh8_Y>2u}hw$rp62k?X>njt==6T(si3V#>;3)6cg! zCKU&%BR}by)y@XXAV|iQ#SBJBQk**+Q=HiO(^$e9lPl)q#+9TO1A8F=N0dU;%wTSr zeMzCY)_m>xv-5*mQ@!!x5%d!Vb%TFwAKXnR`j(tjgj`}iG01rF^ zXA8z&F=Jq`j&H5C2j6~@N%iyW`4xL7FkN_vf$#gNQ4-vF`5v$}b$`RWyY$Srm|Eo1 zi@~3H2UAxl(PCehU!|MfZHBlV1m6=~IKC2_!KeQE0s2DS^B@3epz!uYbF41ttCa9R zP~r+|ipG5n5+XFqXzk`R*_HO_O^$wXE#}&uZ9hZWS}4eF7?h0KiR~4kNsQYF_24m8 z_$hAsk1zY5m$>K2(%=gHSPRTSsG=jCE=nM77yQwtiynB_9Q zIH#tP6UNdU_$|cpo>UKBXkMOv{xILe_yDkJ{IE$wm%HTtJGh(dNA}>WlSQHXfim3V zjt9WAO`Kn<+nP6yRKK=n(Y@DSzH&wTU4G7dFOc~A+ZPnkGyQizoxda5f)ZMj(vC;q zRS9^3wO=@tm(_4HB5EcidBHubTinAm2lj~vN-zo4|4!A`d!1*7{yn1}XoZguUz*eWaSI`qS$>sn1f-58L|jA8FnTUR|pA2J=y5 zL|b|TX>(pZ0E#8QKL7?a@5Cx4 zzt(89Byno6+?A4ij<&FE5UwljOV)(j?NyhIe>`5juA9RlKk5{yx*$grIJq@7y_krY z4)b^OkYY?1G5u_@@BqMj%4vG$sE_~ZYc`j>@$ip3&wwsekt`2f{IA$~^I1%YFo7{1 zOEQjhidh8%KkuVvMf%i{bm@b{F}QoQm5gOI?(jx1sgaT(eQ|c7KTlW;3q?%P#d3OGxoEtRr z>C1uMq+wjQbmH|a`-rY~yI}hphcXXtWW>AgwCNrx}zy?f9eXKF7SS1xk8aL+B#2~v_AK9I6?iu9`GJbc>W+R z_Fw>!cO7xEl9ISj{5Fff4Jnp25$UM;Gi^-MkN%V|m*p>pC_7FP3z{M4)^{c~ow2o! zRM1p-!yV3;cC29wPM#~4dWotZ>Xua=x51U9ju)Rh%aOws+XoSqDOK_+e-aCronE`} zt%)C}qo^Z$ucH|+YAzdZ%;!)Ldhz_V(&NcnAg!h!(b9Y4gdyu69&YWdyYBNB2JuWD z!#E4XeKcDw<&-kcRaPUXi+O#d>%kFZ0>sV6ZNWnt2 zP;DlE#aOdsUXHm|%bP{aR5}D)W$J!`m1je_t$m1J1D!`*%m~Y-c7`MI8oWMrw30q( z&v-u(5wyK#cbYTvbkkz%b`S?&$+E_4%AeF9N&7N~8s$vlYutKX2V!=?J**N^~Z)h{MFBD;p{4b0x*7U%f7BUNFr2j>(G3mTeBF**saqQ2Hi50nIS z>!9g5K^yAxmyUF5s#f+IyYmqN4mQtm--cC&z#M5wvU=Sxuu^)7Nz-_n$36Htw>^wG z6r`aAQ4I9HWWk`IUj38cD804@n*jtMAP%)HiRE;yT`&5`_B+`F;5Wmf#pQv{1K`(7 zq?+cef@g2mh5rhCUN_nN%dXiI1nmQ0Mgia#y7h_};rekyxvgLR0dQ>e>S*w^mLXm@ z?ip^p84r}SuEvOmgSh8HZqU|^kc3rGfZGlI*Is3iG#F^>DKF4}H4wb>$ zOGX2ZI0GytiH2n`C#z8i?p-kh5D271X7;nVPO@FeMMog+`S=+4nE3W`PjhGR13qBA zNG#fs5;^+G^h5CR?bMT z_{pbSR4c&n=)PXl(FAG}x8>H6{?VPUJ~EUWxBG2$6CN>zNb{$c*y__wH)NSv8VgYx zA5ngvUkv=zj=8u4Nuq}w|3wZ%!Xn~>!nAua_4NzFUsMvqAzwc|v5M&ovLt0iq7oW3 z2oWH_=~~r+b}XartAM~ZUKqwr%)DxjtDfydG=kJ~t8|KqwL4bdVg?m1#c<^m z;R+!;MfLp_rM8bpNgIHC zoZMUnt5mgAiueRXoQ)C!0Y@O-c(CT?=5E>sBaN{jk3%VyJ$S2L92Tv z?)&lT1+(dAS*;bmKTjahB)*pOye#@~kK%=v&c5U|*oozopoZHh!gO%|>V#fXCx5F`<)lu^zTCGG3p8GBB1$aL#d9j+dvM#8 zk4>$64aR0+v}J-xNT7vzu|Bd6xLj@#w0x>Oh$*M>154;d-h0irjjH26QqHRbYvR~; z?S>q?BZadnmh0w2Une4Mr(Tu_oq&=yuq|#WpGO<+9zB}mx_tnXMf#g{J3RFb;~Aw~ z(mgjfEj*_hD}^Z)r%2hCoQ`1i7lG(#`EJx|N{D2ahzuK7ohwmW8*8_xO1UZ422zMH zV;qnfHBPxEQRMV1_Vt^supHvUy2?Yvn7pQCWK%SF#km1-4-LoekS{@84bhgIDnVL0 z`8IiKPA>8R^-p|b7Z*XuE!p!vegATt9QE?BJi9E^=G2%nUwI3d*&x=crhazy@q7;7dSB;Z#IM^@qbIqSc_7271 zbCGX{6pqB%fW25>KI)XlBE5Ru4#U0t(EEi(y?e31%|U#qv&u!-N^cc_x`A=t1PDsI%-BzXs4_s2T9b8_{=aJ zIZZwI#3eFAePrWHcVna9;`UccZIcOw2KuHJQ|3eiT1+*}?yBbK7b~9&k?jA$37$tl zbV<~QK=C3p@7DrOsyXBFNcBELRjtoGMxUa#UYS$tHVm@t+tArqGPV~RnU(85ABn{A zrX^x)h$vfhV*;7+^_>tP?9eXyp+|-539w2%Q}(wf#wNtCovX1cjXmHHYS*T3Mv55; zB@As|=$M^za_Ye5h+L_@XY}*7X}tXgCqY~T8c?b)#^2(D;1l5pO- z)h*ZCXyuDX5#YwO0WGl=gSWATW+>_^l}9W{I2|Y|?aB<0QTSMdbC;nE!KG60|7ok1 zj1pz)g8X?8c$b-IK4&%DWnqys`>36fy>5}qu3Qh#QQoLh2a6A2jVC!lOPo=MI@s`~ zD%jcJdilt#j_tRAw}W9Zf2F)BehZHAO4l+HpsoqT)ZZ@g-Lf=H4AV?~?vJ?v40=of z@GBaNJ&ldM7#t3n*PSV3iT2IhCL;}eQ@xE??i9qJMMbUg>vHl$PqZ_V$t3E1jT9Oa zGn0}Oy7d51WG!B>w67044a#N;nt6-Jl;f&33$MZfHJ>e2)fqMPtA@mZZLobY6^*~$ zmnACKts9EIcO*HTuUpHGB|g=S!c!F>+O%XhuBq_o3nhReGZ3{XgyDtLM!$}?9?>(U zzauSQDq8A1ZJMWJXH5X-Dw3DG6G4DXZI|6&MTsZU>!F*X*Ctu{DhqL0L?+uxNUhI@ z2r7@k;9e}&bTlG5_m$m3qyznL&sY{^^=ND(;gi9~lLcZ<_4la~f$TbW%LZU~<88_;*e$4BW%I ze0#cFDTE`>W+chlMW!)q{;~zCZmRbOPj#DTI6ls8$`{Q$Q)%#DgvvXpI{#C16(zvn)U(wumGJXuPNc| z=T2!*tGV8ZAl_Tz3ER9&p75P#pzs)$BCZWA%oUyvTuF^3XEjcenVN&lbeY(+9(zuQ zb+jnAVv@HC03Tq?Nz4orHC8Td>DM5Q>E5Eq|0%0LV3r>GSt&{w=UgfJ*W1VB*~uhl z#yk)I3fVyfDKp)wh{)Ii+Ot!a9}I$enx)Rd7jg@Ai)Q-OzUBkwj07WP{sV-av9lEB z)pNOFpW=RTKF|16^K)3A!P{sDH<+Esl5=&)Wa1-*>Le3;SKfLf_hSrHi*JjWO`0<7 za3x&1{XxyNd1LEN{)rDe5I%1fCV;czj3oZ?Qzu(@YD1ZH{vdnOV_<7ofxZ@UMEPDh z5w}Mm%OSMt36aWoR;zCNF2|AGKZ@(;db~%%CE)OZ_P2N zB+fsSN$dy5#B4tKp$^+LKrCn;VA# zK7QV225lz5Nx1-ZeLi5l(7T|@+8q%phOF~`ou)@q{3Wh^j7xm-i7387C?dv&g#y!) zQITD*f~*sl7s`S7rWP^$TVw1eB^3Osgs)=t#W3JT|35LSsNfR>&fcRm#wq z6FU|A5^G@CkGY~&0Cmr$uD)NJODJ`?*VZdLHNvh0PX-h_Az5dSe&eu#VvaNT&G4t? z@MAy5y0uI6*y8J|(EEHQG;D>SMS#&iYG{TV6q=$!XDJy3fYbaum=a0o8?r)QY%maaf{<^*JXm5*a~1{#^vZL6 zeTk!qIzC(O1v%|w5+kru+}$yW69(0W=OB{dYKqN`NwkB`M$2hXe9~`ZW0Xcx93&l< zyBUvX@-dthawS3OlsQ>!KM2)g8jZ0)prWrXLab?p@49bX`S@y=Li?)H+GwzH{tBZi zht>W}ghA2_{GvvCQ2)Dk11atHe&_*kMD}~$QBdTnLsEB&7w34&MhP!p;Gi$#JPL3bL9h41Cxg zN!OZZXpvJ)LxnAe#sCvP021UBA}Jr!DLzV05bY&BI?3egLp)S*-Y>xBPd2rSYYBea z&;gge3>c}sb%0ifREa{%k{Jpk(;Uh{E+S(xoq~95UYzT&H9J@46Uc+aqqYnYp06{k zkSX>0`M~FGrxbZe3S+!HM>rQYu#Hj$DiobD&c?R6&DMWj(DU(3Ua4{GlgjES+#|8l_Who#!RTg6D=5mO zPDV~ut|t3A7iRBr%1R>nPrHS53IgBg_aaccOBhHx(e+u{{b~onA=@?w#aUBeU8dh# z$OpheE+%>I&a8r*>DCVzFtq7jc<4R&htf#i*o9KuLaj+hPDBhKJ?pq8)asm|$`aiY z=FC`CDIy5Wr3g!Gr-)6whxrxm_?>JD&GXN{6~EpjyYk$YJ9_?^f3a>)K{_p-2az(Jo`8wx&)_4wkhNv)p}ZEd0}Mt>JqdIkk6?21FrGzKr}cJ3i~$JC)1D{mJuOU+%H0%5QLn@p;X&>0leibmjcu|W ziJ($Ic>^gFFqu@d2;5z3Gc^6gNi*VE&RWCp1TL%WD1)k)q_)p6#X4_uRJ@{kSQEX^ z^lkZIaUwONZiokyjDDw#`5UKzTSd&oVO`?+8Y-@F29_Li&A;Nocc65c3 zu;&2)aoF$yrz=v}3*QgtGf{I;-ZA^yCw7z%K!6Zx6KmOt7ZvQk>HS_NSANRPegmx! zz-C-!cRs{P8aA8Iuah+**P3W$c?A~ySt=|ez8#aLC*F4Ct~xyb{%NI{lJ^o28Q!~&(F3kQF6Yh+{GDp#)I4< zIEy2R7*(NrxJBy_zKTDz>qKsrMfR5c**=f|LTbRic%B{9@{@d1HsMeM*gid(=Vo~X znz#+MepK8wmsq*0|C9Y$f(wgkn%Va5=u7)QRFL$=bkp$Xs?1Z>Yg?Mtx_gi8p0(4J z$6wFd*R~&jG{fHMe~QlNVTs9oI(d_u6T23ClbcBYgy~0f(Ic$vUCSRrj(F96W&iE` zM|0x_d*Jv3;9o}QpXdvpwc6N-sBVfkwmZ4*e){$&Cpj)PWJ%rZXw-vh3 zrd#Am!LD5yK4xKe$tW{W?r0R8pM$8Eg7WBnVR*XSR9tOE+62>g_!&x~#a@yJXOF%~ zaZ@5jOwC}fKRdm;8GQD7z%4_LIG`y*S`IfsZiJ!5?=ck>Jl7T(HeRdJk|t1TP_NVB z+8bAC?iEcuMuwe1hcc+!eM-|@%xH%X7K%+;-h8~T!d~`08$5i%_5{nFX4`3b9DYjX zW+Rkx_*So?ds#iwSk&4q!{0vRiRk=qoxU-O5ugtihU8T#sRp5s?^`FZi(&UnqZbW) zs#Ww#A+DaXVkk)L_>Ip3R*2wefl>-?uXKbsQz=QcKF5s-nS4v3b_{k*>jS_xe&KoR zgj=bP>{BHtS*@{D`-EKoAyG`BqE}zedjqDk-IH-M4?hTOvDWM4+cXhuRb&q4^$!ps z=+o&5Im1w)3^G|F?hSr5G4uMiCTBk2rrITT@0@dO!qDrGMOR~92DdN@Zt!`rcuBFh zvsS~tpVgdtlfGr>gb%o|y5!(x|2Aho&{_#f8hY>{VTO+$MARsU72A4s#&Qw!!Ld@* z5G#>%)1vC!I%%Y9D1b#BhQ8_MIX|KP0arM5m6QjvCWeT93O+U|H*w6EtBI>jhgs{V zwQw2DraKA~gT$Rga#`2>b2q(M8Kn3z@=d?Z(Yi9Ej}}=#h|(wYCrC8>NK{-j0V`+h zJZZ9Z4E*!Litr6-n z6$?&c`|EbIj0o7IPTTf3SI<=1)R_N77xTNtSh zG`Do;mh(C?j9cBk{pU!z8jLOXzBhxkT3WK>`_E z;_dm_tu}SPMXo!Fk4}4y2vW&WmEl)n5!n1S{1ijk`h3Rs-P7;SX1ub*y~GC2G}HYr z{E0qf2_?f;k~WyqnefGiIxjSSy?fb}%X2QN|RdF@I|hyLeuQ(SC*mqi_*cCTBG*{A8@ijOFLGPesa?x!t56s!u$Sk zSr8N7O0^)^oHw8=pK{CijaZbOP@I9|C=5aI7)tfRpmH68g*Qc`vAHLID8#lj|GBby zFiuQc#9FI-l2RWR=d3MwR_-RomZ`@n<&5u{a-p)d-dqq?;n{T0lNK7L>=EhBll#y2 z(tikb(7pP*y>aOGnIzfR(C@@wg-q!qvPI{vKQ@(fUX3YcG59B64Jtg!8BJs}@2|q( zzba&=a7oil(}R%v!r^!5zIhV(2P)$rFOjc;?Q8Mv?ai4~Hwu?OKZbn_gkc*50;EYbYdcB4I#wPRzGkboGsiOe z%isL3LZQLSYsd|4M*Jp)(Ghq`;n)!m=w}qB9;tFx7@sEmt7$Uk{eZ;<1UNV{o-^Z5;Lom0N z^u$5!`oxi?^bDOULQK7ej$ArkdC0JN^V;6LV44b0QYQJ}0bsB;axp@z^p$pkh!SPu z0q|vCzAkI+=p-{z9jtq1=8v|(Tv)bR(5VEEJWp>KJnx~~61UN61g;9ecl3@BuiQLN z+|-jgCYS3!u`k23d0g=LmujM|94Sw@inY4tebcPo7QI6zLW7_UrW~$qTUkWkwJz0# z9J9CPnzxesRu^e6@!D5EA=4fwNl0cnhQ~Inp*NDLmQ`S5MA0*_3z?CEhC#eC$1p2H zm&oyjVP}5#oeiC+Mg9C*=cq++b*yR;P|AtngVW)Pqd`ZLjU`h~LNT7o9Wonfhn=Y) zSNU8d5Y3cBrC>%jiB3fh-{%$M5re_!X{vHvG9xD=*D%uI(HVihS(91PFQS9$LFOu1 zX1XRj_YL_(ylf~TX>e(|;VH~u5h}qa<)RjnCYK)1X<*;xW$94jN?)`_nl?o0t2Hw? zQ#+thZ7woG89eF<(Ri5{D!~iPv4=)KopJSFd`YaT@-ClhWYsH3=;=G}Y~~Q8PRF_P ziHV6>1u+$s{5j-2C8Uw`EG}k}%JxH#jqC(`Xa1g@sem1dt(@ zXZ#=X-ZCn#E?XOg5Fkhrg1bxb;0_5IpnxESyF=mbBv^2Fm!L)AZV6Dh1*yUb?hu@y zx%KurZ+G9@efstt_l)m5V|;tmk2R|Ho_powr`tsG%MpHng}-w3?r3iu zRkMbWlq@bi=rX=*fu*hnSY%}v!rTDU!5zwouvtzw$>5AIPc-Lzqm88v;&iRM;#Tkw zhrGt(zN*jA+*(Ao)xK`|pdaOwh}u848Z5LP9xh+3nLUp7o#%<9I=Lzy9-X*kc+S<) ziikVr=?CvDA0k29WX`I3tSajQLyb3=IVMHEOb!&vb9z|@d%cMs&x*xMTb4+xth6>WE--{$Bk?=&PC=cle<85PpKwDZ*zZZMk3YF1YMe zz%{*fKy`Hf8(-RMZ3o%Rf~NLPGQ}3RS=^Zuj`I&#Zl{i~lZfx_H53_t3Gx-~Dc2+f zYekR%b-4n)?Ka9ANcxBT&EaU+p`@1!YCN=zQ}RwfJEVI6o6ZBN?g@xT-Ktei|xs!0QdS-Yxo z%u0=;&@Q43IT}OxelR6iRy_v-VP6 zvbW=Y$m9c&#B9w|MQx^+~mb--hsg4K)tOoM@8roUYuYO9vYEiJ=^@~UHhOZsfF7MpE#u8&zq zw-WTjv{1ACGJ4TApJbu31vi700A82$>^uz3(yZ<;w~Gii%s4sG-q?tsS@RhQH(qm* zYEZ#vSy zneu-=AfT*E&x4{{0`Axx=lYkQM1lehU^0Lr}W=RBVxxEs1w#Kto|{5QOn|N1@y z_O#jdCm75TVC#;(h@34{N~qi_OaWVWd|W5(PZSZ1`yaPf&iSG!Y28%U=`OV!EkVq>DX11 zQ)7R=cfW9?%9f+vw?Zbk>ZHYbxm{0laWhJ-EtBGa$G}CV;_&h2HxhaEb5O5cnv8pj z8YI?{0aMmx%VM(K?nXV9s8jhe7cgZpOmN+~*^j_1>}5Z1FS+F@Xe*3qn*6rKmF|;4 zsVo$lIP~5Ap@PwV320)PrCcvL&2ofVFZt{$Xzg{VJY{ug&xdc*EP|`X)Q9s;e*JiW zJo_*3v{&AAksjeO?Q|zUxjCOKBJ82e#l4kw2SvvH2Sb*A2`;rO?k`_tE-FUZwJL;) zH2V915Wn}zy-zK;`yCexxNa%z+^!AZ)PBz`(KW56=HThzZNIb~t7t5aU~>zfbI}~( zsG(;ZP@xM^Q~SW|KRs9VWkBD1>)bh|#Ot;Zj*d5mRo3kN*-lU+rP6M?rodc}yw zKski7*ucESzL$r~U>mVp#L^QDjk!f(o2SBCmahQWEtar`?%gd)T zq)qD3U4mcS%{auD>P}TIR>f0nWO&PO%B!rie^K3eyZik8DJ84RY6`4V=%tTrhHXo+im5i#Q-y}W=h=E75)S+{;JHP8I7a>pEOoIR+q6GcMo-WO|W4)tTMD1 z7;dWCKa=&$E9(s)Pqg!sVs_l*br+M>s>)lgA99nwavVQBNB##d+|xxud3stJR4BLJ z5Wv0x>ibGKrh7=iQcZ?Djbu0o$39|>J_?%6K+V3!AbW_rMNT+Pb~37xXzI?8c{*D@ zy2LPg%AlC);7ae;YJ~)SX2)3p45_(Z)Vdm0sq;P=r1(!NOL?XfWcm4 zH2PvS)ds>Pk^V)*Hv3>^of}o;Bq%B*94oX#pOL2r#6g@(h%5``S>T!HDlu+I>f_0M zobY++D82*eJLM?)TE7y!IMF)&=Kp)U6weRglB+Zov6RoIdqt^}m>wP;*DIE3$rRx( z6t%;Hf>chM_=?zJQ=01MQ(4;g5xzH*6h)K<2(!t3-}9%Y7-`^L;ND~DD#E^)EssTX z9QtHi)?9%mYmnT&gHfN;zxl@{{U72h-+UsQrIbc< z4r5)*oKn*Qxx?#Y8ODNB&8KvHzEt{Tq_8yAt+L2>X<349B5106)VQa(*V|kRg&Uvu zyi|8rb`C!#nGHSQXB9KL(sQ!IVS!Uf zr0NS9eeTb3ks%WoV{#-k21DcoO}kf`5wWCw%}+gS@U&w_&&8 zo3;O~!-YWxq$A$-u4@osmyqsM2G~D>Hr;Z>?S%#GBVYPq-_>vbHQfnSa{3)R%u|$-V(Jz zGwgkJ=Kh%!*{HPicp;zlXVI7F{Y5<&j;=qMbZEI?E<}W-I`t+Rs@|hm{<2Xs<-d{o zC^bxKUaa%is&%g(uEm%-Rdy&fBTsb-<$)b%#H8Th{ta$Ch-x zNqybb48aAq0bbEnl7ug%!A@}s-D@mv4}>2|9BdZ@9_1@*B$Y&Th(-=yy$^>T2aF!e zh#;Irm7`fe;Htj0we|BS3-=1yv9c^W-(s2IDH-A1z<`_mrW4d;bL2wjS8`!0P8q(N zO(ax(QJ)5WK3Kh^!nJfh)jyT`ra+!WBG_XPQnsHzT%%A>)@s+<6zi*oaUTTLtL;TT z?k4hjvtLtpx=;AQ-(gJ`XFofXs8Vuyt{x`@?c89-0v+nYjU9nZ5;G!)T4nS3+x+Lx zk%G-@D}qfw0x0f05XlRT&s(DfbFE=swMdFeu^F_yA9Rm!Xbp6Sk2kk3)8O$ytEw?s z^<_{_fvs^!uu3f7r@o(S4Ni zLLHmVQyuDwPw6hn-APP0g*9pqMk$8LIfntB(jN?p+ZjY#)lHh%)OBc7xoXG?lIBRu zbj?t7&F(eVf|WT7TVsfhKW}nkTA%P_=$WZMZ_d7$V7uR{1Q>TD2^(8Fq76)+&(viN z%&f%$>pR89h?}I!OTsm)I#cKGTchl;UI_h2`NUpAXB|UQF=_^#ANez8)41vk^8X>} z@Mk74gk1U`!Qyy!noKzH*)N}sDxdOno#P!M_B>1s92NPY`zfbM3~pPWJF*lqGmcMQgCBaP z38rdeLanPXvrue8 zY^+O~T94SCUyd*FX1uBgF(nsc`*1m;_fBZ{Moula|)>rPTv)$l@6J!>A%6d>N;N zZUR|@z*VVJ?TQmwE6uR18S*$xkyaIK=vuwid$YGhc$@=htmYmM@CUL(G7H=K2R!u4 zC_BTl?BuJSNJYvDb*wC}8LtR~Fb~m~z3zk*b--`#_}R>lgL=kFb({sqK2{G!AM1Iy z%Z74ZpyPE-H#mxTNRKa0)RG;ESifY7ZjN+xt90_(Pw`F4SQ%6F4<8Qgd$+Nky^YT# zBfpIaaUSq`6}*b^ngSzY!Bw2OcC~EJ52z**{(iz2i_eVIy&|QuI4b9eK8vsJsbXY& zAtUX3=w=XgFdk<8%4;Q`9SS~Rv(QX0oxu=hBN!%rPC}M`Ad|Myu`uxpf0p`*sh6_y zvl419f!v%{1Z@({=uEOC$44`W3)&aMFYcf^VjyPU=fWO+>F4Wn3<;v0J+y95V@uVj zrZZ-A>s?Z`*10k29Vy$Ne>H5cXhblxmgCvl2Sr-=wQK}0$TizzPN4TQAvIl>cnzhI z44`RFcy_oS%$GZieBma^j>t${);B4kR!0F4s?z<6R|Txvr~47gy#a^!c$LEl&0hMp zN>DE(SAObDNi%`H`>DaWTcG)^WgSyv_fsM@o<>i#`jn3=+9)c~jDzeru!&pXjIG%* zDvQadTO7E<*Y^!_6(?dHNB`{b4RatO2tNj~xR=IGRU+{5CHLCDv90J|W|U94caPPb zh@rS-gDk%c&pun>zn3BR7);Nh$~;BVi^Vs_b@59WdiO;LOD)T=F$%a$qRrtCwxfNj zd{QCBp2RQxI+%p>gs82sZxLg{OjQ**uA;B0Qkve;6FgiO%iU;_N`qL}NQggaPhbGD zkB}ppcGZWMvrk2lQoB<6@#OJoA?5YBiLC-IQG=ds1zuUYd7h+dcxaYL{dGbiZxHZ3 zpS=T;DDBAViD^xV*W;dE6-k6opoZV8^nyugm8)o7MAhl~8m^#FOu7B294SR2?7e6; z?i<(kckgr;dm4e$tO{%?X}mv-#+3#2hKV_a&R&Vzjea97xa156>5KZH^3(Jhr)l%R z5*PBr(cgKKj7Njk%&-_k!L=DxEHg@K%qio|wnDS*FNMC-=jXz+dIB8J*j{^6l66kpu> zTlAdIsn-I~8iL2C62iw zp@MNTm%R@1rz>b&+ECNSxcw%7$J7Ttql{hjiuubfy$e`?RX!!y3V!1_4ZO+y=mW{q z+xn`cwe5raf6}SKJTH{=@V)ICg5Rb(zX0P=o6)Lvs7J)C`^PRL+ZI33XsBW&EDj1{ zw6sJvd&0#dGCGRwZS_a;^&c44&?^v(ScLzT{J}RGEKry*ibI9|3pRuqz|h zVw(ZV0gyg#waN`|JB8*h~qAfHUo=uTA=A5m<;ApsOx4y(G3WW zLO$#fdeKY_k%tGUc*j>Vb@sHB@BVk7fyq>5kwn5Tw&sAjMtIAPx@n&?qrOzfAuaG0nh^uZe(fzIXK?m(fcS`uftCS}hQ3llJIt|6 zkw7zL_@YzhAVhaitzIcD)w93Ym)ARC#GQ!M<-CPdBsw07Z%x1RGHcwj#Fg)0gJ2qW zaZSh9us*jf5>T)0fK;8P*0E4)6k)w)GF-y=UfJs@p?E0#rG%nr7NCL<)D}}-?32?M z1evKF0>6;~ZLE0^fsB19Da96nOL7Li z>i>9fV5bH5`i+zdf3W=Mvq*56E3wu16~PtT)aP#%cVq7eNANGy8tH62>gio(BLB}h zTgXqtV_b#m6-7OdN3#_I;gQb-g+c*{!x4|7mP=ed;>y$wUHC7{2y@*NhNSLpY<$=1 z2|MYlYqYEWNBQrBC&+jmNYH_Llvffr4QREXk}M(5ZD%3yFRM+F^sVn9Q?2R@_|nOr z6*y;WmpS^pL-Bxg{RPCYnUNB3F(+Cy!pY=YMHANEy0}L7#<}ToDUVdFJBSgYHT%@q zU>!w%U+1bu=Xxdb^4U91u1W2~RwM7Z8gS}Qv$_axj$-Ka*Qml#f6Jgc?xa#dceWKu?vEprqn6{+hbEV!O*KRFYAE zlKB~H#V^;`s!QJEra20Hv$Lq?IH=%R%AR_*JH?R_aebU=?K!6TEJH7~apBXJ@E|Nd zD*0H_6C+D2)?|A>g%e(&(*de&`WNk}XBa@+y`MxIA@5R^&F82^pvvboPjz+^2+X+} z&s%+RL=vMznxDmP(*awBKy3X2bjzJ43e{rJZU6}q`KatjuKrU(1HNg3HXAJ&LK2gC zR)Qma@t+DQ0scM&T$4+?W(V+cdHPIKwI@97&mt`?-7wzO4)^$)Ke6<+G&gHT;A=w3 z8}H_PRUc!JwyIZp7ve6g(b)s8C}#qT-U@-PYp#kONRda3IzKm3?S6lu`*0{$_8Up| zqs#11lOwrXUX;|YzAtShCiDM$q|j_?eAQaHPY*oy_&P`BC9(OP;`A@hG^3CI%$atu ze9)?YOpol@Hg{}p@*KHwNc>o~m9O^+SpP_{p&x)zJ9j4<*&dU*qWD3(NRH)pjRAlz!M30rx_yO5gCs;D2#KoDAN?}cg9+fRmn%tNWd z7|M>w+xXWXslf*--*u0Grxz%Hx`zH4$(p|P%iKEc-$+CpraJ`hyfcYZ;Uv+Cbt?OQ za$IwILW)M746T^SrX+~m>!?~s624zKaax|+HN(;^6^$Lhu+)m;1ORU25;a`?u%tO7TR8lB``3d{Q)wLaY=n)1^Bh50%2a>k6yHM+vOdqh|RCIt>h>0 z_LtpR-ykpPic{NM)Es|>v%G$P(df!9!du|2QeS0M~jgPeD|6F@*GjijY) zf|XT!8ef+pxBc+y9K{m16xG>*&bE6C&PfZ9<%w~x+T^$w1f^s|@Dbo~9rXPiW`BLi zf?BwFqyi!}K6b1#$9R+e#9iyl#c!lR7l{rxl;Da^rqV~LgollMath#{n8X=*+)oOm z)%|Muz28Xjn}J@t=;%Nn6_xwXbaw-91%<&=K7pG;R4c;B-7v$X>EB2+gO8`*U%aTe z$j!TXA$J}7(dmD%{P$%-ziRNjf4PEd&(VtYDry;eZ^TjD2RLZ4Mq0*CfAKyY{dauM zh+QdyPi2+~F@9d&w@loP`JkBnjpmTlK?_P5*L^8YR#ewIJaZc*Uhct3-t9Ir5DhOg z>z2{2o#!L|qHV%X{hDH%Z+Yt1(Q(s+dSG$M%C!6)B#f#`Um;yvAqiaGwXo-4Q$N$D zm!r~X7I&;3twQ{5sc}j~>YvPke|y{`IANZ<1fDwmMjDC!jnpxl&oA~TM<0Ht%nWgE za`%>Sl#sQJsWSnI%TZ7SZD_SM0fSj<*H~CQM1eR71VA7$3;E_2j@+BuBkes7_?wCg zIFEMI2!GLMd6A36__J5VG?R|=mKA@Uj`G9ZK9L)Kelk(!XX>E{;US!#Hqe7+`3s{{ ztT)SRmeN1w?IGqmRM+lYq2HuvN2W9p_J%b)&*KMs^SptdA&lKI1Y*FK_HvuujaaY+DOvv zUaipTm0Pvf%uT%HzoxI?FzsmN-?NoMzMqOdX1e3p9}(j%K~wuEr&g`JVvq`PwXv19 zsz%GaZ%YnXRyb?6jFYqIcyvhBO*uWjSTRrt^jL128X?uoJkHqj*`=8L2U@HrlybXw zKk({LnDYNcPS2B#x3&~k)7QdjR51k#Gn2`eXn1tC>p!=+t`elEoa~k(+51}27Ck=T zCZOZb&L_$;^rG-X!0@5f?5>k+PH@B{$qw?DJ=={8INqMmRXVm z@@`Dg^?xV_`oO0>-D4I;Jl z5gH+ElPGrC6!f)_BOP9rfu)yn2~igE#(0&xmioviy)K2)^K%C&kxo&GxNe8}N**CO*Q$-_KrJRM*W4=R|> zz_yp3DxdLyC&&JfT^CGu6$OB>T955*Muz}bqvIWv#2o0g1BBiahQEAg25cw-Cv^&c zBbDVyD5V^r>~B;|?^lu_;Nyzh41DnC+ussYUA@AJ3)dGzq717Ce)4MObct*-+*^V( z6zq%2yFuz;eNLkZfZ!MJ#pNn+r|05py(2!HC3Xd8ACJoo#Q;6C?s~J`fWq9aZ**dx z_w6$2GWT^v4GuMFgaDL=8E19Gb`YD=KHUc*Xax2ZzMWJ{GT>66UCcgg?Wl2Wo*o8{ zany=&lpAl-F1>J?ma_u2O|4k7`;!3eOL7Szsf3U;R^aL^%y^prH`3q!vbIv={~v#v zm{!IkW9yXz-5;iC$HXb$M7=W=&-a&qlmA~03Hq6WM46BF*;`rVuLM~}i(}uDC)+Ww zRqHs!VjRUp()#BhbN*XkN#{Q%Cf^VuI?#)N1M)nc8-A=Cu{hNxckk!|(Wwwb6_&e? zhDr8R%v2Q|Zh)5F)~;C6()n*&(h6kEa>Rx;?P#`<`7GSRoxni$TYBgH{XJ`&P6H5& z4N%d($4Gsf{O7Ouud(~1@4oMjLVi3y{u1UY7c)pFk;2T6z2%hNbe1XDaPpart=OUE zscdH$dep8~&wW5H{8l7qJ2r0&Oy95!myt-lmM^x7ZaAClX(kudI?Gx-nAMW92M0zC zFLr5V3~WR61q>(o)n>5+KJOVe>^U?Q2vMeusg2A&S*>|y9y-i%6j7a!ziY7MpOBsd z-}>a?&A0BzFVY!S8s}MRt5dGtUDa5ZIqwO!zVuieYdXugn$=Q_c2+4;!N23pXaJRY zyuX6LigacBeFK}lQc!b{g5JgZF^Dd3 z<%orYeKl?7Mxu|C(#Mx&*dKjL2jo9pPELG3t8c1lpHPZy*$ zl|_{nwfT9`M=2S9xv#SuHmT_SQc8~I+E&%A25yw&PLhyaMnovv9_cC8Mh$Xv9frzt zIekmPhNeE`y=%OQ$<6EQOuciwAMa$lR~{$8`)oTLRWa-l*`enr^s7BCX6eT!;eicg zb*j!#I=P*XS(hBv63VYl-;&0)_9Idgig_LJpjk(QYYb}-`trt=(n(rl?EkqAC#`pShaq=AU z6@N=qUCf~5r=oZ9)>QJeEzC$wP^z(p-0dit#++L*t zWq!0$X6(TMA?A$6UKO9l4qw{CWS&fa^<*lU^npYD_? zjRdJH6+E^JG>X)Oi|9H8+(J2is^+(Tg}*N-Eo&a_!vV*CCdsYC5GaM zY4MNHP4G5XvP+|2dy`~^{p|W;q}oZa{si`@NA%Bm9@3i>cGH4${psyIw-c+56ED-R zwwn(mCKZzZuIfivF@DSID$$p>MAfkAOCO(YIRVE~oJ>MT9Ya>+6&Iq>1H9wRWs8P` zYnPo&!XDf;ee48B%HxSO8^|gA#($-gw~4n)k3OeS$V7Kor*Z+PRNg=R>`cIj*`IwM zF}GqG1w1QZO<-j|BN^O<^ls_;CZ^ul!UNGz0MVxoLY^ zS;_p8{NaPgY-PN!ycE`Agn^7-%XkH_w8TrGlcs&WKa!-^^Osx@osj?5=Fmepsw zL@&_fD5hp@Oxj|rWKoG9zZ$75nGM_V1c_YHQTP+zjF=UQND496qN3M8@l_xQ0-G3+ z-On@9E(7*4qu)rG%_<^~SPPH}y(k9*no{mK;fWo$YrgsEf${>2$2+i&)+tPfZ6xsn zgeQc@v2^uOuG}!X)X2hxLdBBajzc(Ss8OK^%k&4u6h7KlfZ2PdAvR#@K%Khq?vL$j zVA0gC+r7gJN~%3hU!y*-%}k&iaPDY&z3HlMW@X_n$0F`F&or>Nq%dl~@-DZk`o*ov z7?2?E@2POXx{yJ`(6Sq9E{4A^h4C}xHHhe`f`?iGl!nbZC7|rWo_}~g*Oxsfy*n&HkELFA#RFP!$%L64K|ti( zPn;9?b~6g6(lg*~WaRaw|9lMeEB#OkQP!cIr?0=k%e_Fxh54qH+(LDGtS)}VsOjbh zCGkLa{{t#c&qnw({IQ@$@#afnry#p-l}ho!4d=*FbK_ctLKm{JTlEo zEKugWsJL3K%TH78nYgotI$i@@>?ikRP()hTg+P{Af3MTkVsdJsd`UJO<^{iK#!?%> zpDhZ@G#INokn=8VydZP4FUgG39Eq`O)vZSR>c(R~t9|&FXWI~`^9puJDREPg6{GSs zP}Qs#DrIEf@Ik?{E4Cfk1odwOO2ntfLpLNSr;E}DBUjZ7`$wz_N(&y;H+OCbtc&rY zK+6FN7Ztye%FBDesK>UVV=Ftlw_m5PUfnOMgG50^ayJn8#F_fJs8OR?Uj99QpQ$r# z`vqg4)phe2+Yp-aiX>c_(IsQ0#2323IwMrh%v7^3jb>u<1mzV4#mcpF7oz%;K-CoN zLk0M#DhG5mt;E^XxJ5k9K9bbjw6JUMCXqU&Ub4f}#Buz?z3Mj-PqR?Z8*S1^1JE}> z;qH<}Q~;KxLu9QtIvJZt&tdVbQ#C=n^APVXOV(Qzz@r?s4EKU=Flf z!(|-t3FA{VF);uqJy|*eL-s^cRRlVaW+dusuK#Zu%jdt5UbV?yYp-)@)TRZK#P_>% z5Q(VMju0Wc=|p>H=&%r2vThH7%ul8p8?^|hT*e|Y9u`kmIA~|c} zGi{lCJ*LhogtgIJv7(gNXOHnGW);G|kb_H^Z$V#zEdAyt*I1ScNvt_mK&|Hb8 z&t;mHs}Nc%QlpZpAtO_|@KgZrPVm@g`Wz5YW`e;k7h<%$S)VPqp{l#za+j*(cL>YL z`N05UY*;W{O8HQVGI{RL95Zp5qPeN(HeN#1ss72r!GJ$ZvgK5hP{V`+kV>!KCo(Eq zP-J!2HL*@vcw?cGZjHWt6^L_5G{SN}@K*e1{jbkuT{>FZ!pPTtJNCE1WmYyT0?o=E z>1O~(NsruRq(84TO5go2uk_D@ipiV}f93lAviQOh8 z<2#9PpQx>r0JD55&-r_)86!6EImO=#f0-xx{v@z+dZtnMx@RT7SzXXPabknYs|+k; zTGPY2Qse&4&2YMi!C+~V`@Nt7Q@Hzu$jgjHn|uF9p3Msgq`*d_ZTM#)Bx&yiWOX$j4e*QLqpTbSLe$92u(y*^AN@G+b00Y`EO;xm9ay`ZVX?ZWe_J zyGr z$0}1uFTdB`TMCVpU}?+MGjNf@nEe%5Y)V007|f27bo`ArTA+rCmgXS2u`W8VosWCB z@AL}WiiBoJSm7$<6q4jbKI&*ga}-`jFT~t^Vwb7Kt<@@ew+9bf{YU=zUK=Xz^S>wa!9#d6oqi zyypf=Vfvq$c}e2ysqe=rpO(ChHJyr<#Rwt2zPkI3^l52YU2E&=xV=wblt8J)R%Pch&OZXOXtsf-KHG5 z3&b*z@(a?4sP@)=>`o*mcVw7p`}!;TzR#SO_f)8=+wuBzlG+yrGe46GdGz%$+YMyH zO1V|-XwU$N$D^TLoV{uB($8q-A{9fkrlN(ufKz5<*MV69Ty8DdC;uLErY05#>DipL zsc6D0()^Kp(kFQWAA2t;#Md$l^k{TRUutWA;8r_XK^~+fp>HuRq5f>TU@33}(`>Y2 zwbc-`SJ>T%JUpQTI%hB;%CN0ns&`+no4e~?I8BrTO*1cRf2wInL}Fp?JmGo_pJDzh zWb~iM1)eqiRLg(OI?FOm#-E^2t5=h`VR}>)3M-3n4&jHTs$jxJGSbYY$Fuc0I3EM+ zUk!WHlJfjw)l^q*0^wP1MRXGE)6prxjRgB6T|OkUL*e6M{nU@dxvGG7_B@5x%vCH* zdtBm4A(t=FU!rqeM#O3N3*&4v{j?sRZeE7VKK4J>8(IEF`ux^T_aDFme?<}eN8?{$ z3I4vm7zZy}9z2I_rD|ZKb}gCQ)Yht&Br4vk(nFgY=&WhFw4P3anmvuBjFeFmfrYht zj!A3f(yP|&1+Oxz26(NmU4ux}vg^ha)x#|C4hBxux>}^dyswpiQAO^JSG*_&XZTUO z%%0OtU)FlRwDAWe{6`g3XhXC=>w5JQvs_!8|UMPlDt^;Vhs@OH#T#K2rbvEE>c@wT8X{qXat znxn(BJ5o;fx=Op`2jR!O7E#uW2gtx|0j_DlRrHaF|B|`mq2mGHmE$irw1I!{um3;h z#lP)tN6dqa-Hrf|MC3jek&CqlPwTlg%-y&kCE8+d*wl=cz2ZQ`%tf8Uh^BmldCiL0 zyR9{wrNcVcyiJcCoK;z|FwDr6o*dl*R81;;sqdtm>hJfMhd;xo9JMt_+-v8eF8F1xvv~f$*HDWg+Jg zyalfSnmaRok`j>t;tP(){fnKMnLsJ3>3qKC*^`AXO;yvMDq4e#N8(wcW-jjds_CNz z-T+IX3vwj`MNlo2bq z)HznMzMtfuSsU%f_V@j5e|-XPQUfitPB*&n9+{mu^tUsq zq6vwA*=)y>xlsO=yjf=)acn|2YCUxFPI|mA4%->DpK93&^q(jkKoHk#;axbg1jNqy z{NFlTfO&nfBL}95)jKdta2fu|7>eS=;&>8dWSs_xW!$hS7uACT%AS?hF##EWxfq>1 zKmr&(}1ngKtsjR zh$L><=5u_EpwWs9jFU8`CQX0Y>C2d#Hl4KgAAH2qsTuCsPl+XTB}sNr0ACTF$Y;Okl;J8j`-exz32N7J`bG= z`AAv0?WKFIU{ezd*{4}%#%&;yItn$XX`$abahAs6GAJh5T zo?hY6(+V&XCtm9@IClVCW51Rjzg*P+i3w0)=0E&@w@nlFUyYW*<;$!SpDhMZD10$c+dt+OWi`ZqHO4eRbSoVPPJz}+MufpS+yh`YXnu;|6 z^MN8t5BQwD*Qr^EiI%T0nr&|?j9V=xSlNLHPjYk}xAx(;axH1D(c$&b zX9Js5Dhad)DX+`+A)(wRy1l79IR0t!og^dUUS}Rk{A~i-;C(P~aJlMm3%UvXjl|-T zHZ^T=uT($HhfOLjmH+03qrT65@z}CgBm{zA+Y?;`j8-B<{oelJG@eJZr%v4kW3!u4 zoIb3E4?MSgqOHzYyyz7@z*D#PmCT{TIUuG~bJo)cbfA%7kONLR zuQECtH}53q9Qjo%#`z(vGq7aOye+u&Hge?Xx++ssp9_}Shsy0>*=DO#Z|==~%?AWh zw}fF1q^r})^wNQ>^e1~@ad60I@cY(WKvbBG{~B*sq!EhA3{CZ>k4vtK*mUPRi>vhs zK)-hFWpW=XPk?FqcmF6UW~Ro7@_yEE;}BQI_X@`5_Iluk0Nbrwh2{-DBPkt(tfSGa zpBTk=MkvNJ+%_atpL%!Nicny)4wkT#y)L5)$yD7weIK)IxZdXvM)zW0b;>)k$mhfv zkiMM&oNZJtjxSlSg$Jfc<7UMym;|wW_sbm{g>Hvk4?^6+W=tJ!Q8dObVz7@LOI(}m zH(7P}Kpqb2n9r3|8#Gl1g|82vdv`2(%VoRwefx|PU8C}mUu)5`ti(X~D9*66G`P<+ zv~y+<5yH0>Yr*TaUWVE7@86E;`E68~bkLYs*QNBcCD!ddf4wF~ zDu(ib6kWs1aXQB!yZe$*&4qQu?VwD6$TFs-QXW$$Q>X;SIYdGs3a&j+>T~Qi?Qj}( zd?ICy@DI)uMODueez@~3MTC~aVivBN;yyhj{_wV8zQ_0B&mQ`^Pt2QJx~p#mH0MgI zBjT1fAV$?2n6Y?QoKcpYD#2q|BA2In_0Ih){R^)`JI>Ijc-na87=>a36(+)htISsL zr3%4Fg(bwwo|9vu9LL4S~doZV1{ln{0qET{XaJRe^=dErdhUl6?IW^1xwKQ z9rPIe6&(~6?AMd?-T!fq zB;2iE2+eOEXla%CknIfjv`S$Ge zSiJy{Oa;dnm{;2J1jF{G8FVvKh$fo(wFPi6m8DY8kC(WcCYYV)8Sq3^kxUhD*R!?M zL5#n8_IR_XV|jMw6Nfc?pX`1Uz>~AC>c)+VJ><~7nPQ|ju`(3y*<|hGYkL?7pNmXQ zj(kBGC*h>!M{L+lV14GmW9XBD{Yb&Jn^@0Kd*s9877;Q6F|Jb1Qb{ev3V(P8u?wd+W& z0N~RQnE*Xj9aB3^)q47;pFKn7iI(d2&3a9#-?`ITYO$}-y^9#z`4t4yuOc}3@&$+Q zaAPXSMl`l{;HB>%W|B@Yhu*k(3^~$Uw)OOn&`z=(b)jDQ4GqEf9V~ib>io(LGoEBa zIp-m)!O)ivKX{9?TPVU5-$f_Sj6gBn*iZ|=@ipy^GMb4XsY99|&6I>Xmchukv_scA zZ{H5G&?JZ@kz&gmuQaQ@x%^C}v~&x>Zg(c1*UdJ0ihSa6;oDP?7P;mN=`Nd}x$X zj;{`uh-p@3%?TM7Uj<6JLV;AD0rMaxmD+~Y$Bs4nN{ia3GTq1#!j0o~Al>VY_;5{J zW3I(ndff?z3K1Oese@sg{R;+7I^>{;ZEkdHk|zvd?yX7P)}*Io3+kY25Kc{MwYh(-Iwv=o>-{b>)a}KjqDb02V(swa@TP$-M~TbI%XhdZb9G(Jl;)`Y|oD# zoC`}37A&N3MJ`aT>bulkCfYa0)YV?H_XU#f$ADrhO4wqwgrjG`HnGlm0zsNR;p@+c z!#{B`XqLmj&GN&wNKZ|xBoC^}INC_%>zBf@5(t&?cU7oJMhOqE1Wi#W&DBcRpD5Qb zu}3wOo0arJU>^jZs(twMu`+}$(Nf&6e*pV+`Z=1c6fP~Z`?Bs0i8`nj9&{N=XSwCk z)j8WxY*=GA899bZ9fnKLZHq?|6hRX45t@GQsg*07g42d5-U32Su1k}4InrrxsOuv; z;Jc5g?7=*#dZI{g>507fE&?-^6>{n(MWMV!XW$!7TJ0U07%knc%1+yh;9D`xAdUuavxGv>iq!gsw2n35DcTzx$G&@=-IH_uUsL4QdQWz~n*gd1H6ri6Ys znPA~>!%Mw1&ts_?FkJ35cMPkSkCoN>kb&(VM?oZSxms?W@SY}WLYlkAVsY(wm7pHZD(>${Y)VK z$%|*}B-l!yU&Iz*`sqZ$^t@9)Au>mCBQMrbA}elq^kbm|gh$LY>qnnH%^9m@%(NA7 zwt4+}-w9taxVbJqs0$;TAjMPe(d8}QFo`E5Oh*$ma}sCH_#T3l?$DUUV9s4@t@G~J zHAJ~ueyNl?oxK20CWU1gI|7OaP`0dFrxnT~3?fMpeC%Qg%hJ(RFoo&1$ck#v?-F9` z*R^ZzYnx^dPu2;Bd69MG;REc56JIG-D%>=V=VvxDZxY37&zVFl_)yz4eaDp4t$iI~ zN1hx39aO>I<)YRqn$4fgJt?SY=kdW`yv%Tz>4K$FC^}*rF@O{nxM&jy20wm#KS8(_ z(YCt5A(+XP%d(ReG7!Mh?t-x-(-78!@_%i=exB|k_U_uWwn literal 0 HcmV?d00001 diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index de917b7..88164bd 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -826,6 +826,7 @@ class ApiConsts { static final String removeFileFromFamilyMembers = 'Services/Authentication.svc/REST/ActiveDeactive_PatientFile'; static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; + static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 5fccc6e..b34b47e 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -1,6 +1,7 @@ class AppAssets { static const String svgBasePath = 'assets/images/svg'; static const String pngBasePath = 'assets/images/png'; + static const String jpgBasePath = 'assets/images/jpg'; static const String hmg = '$svgBasePath/hmg.svg'; static const String arrow_back = '$svgBasePath/arrow-back.svg'; @@ -206,6 +207,9 @@ class AppAssets { static const String dummy_user = '$pngBasePath/dummy_user.png'; static const String comprehensiveCheckupEn = '$pngBasePath/cc_en.png'; static const String comprehensiveCheckupAr = '$pngBasePath/cc_er.png'; + + // JPGS // + static const String report = '$jpgBasePath/hmg_logo.jpg'; } class AppAnimations { diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index d4afbd9..c167aa0 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -55,6 +55,8 @@ import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../features/active_prescriptions/active_prescriptions_repo.dart'; +import '../features/terms_conditions/terms_conditions_repo.dart'; +import '../features/terms_conditions/terms_conditions_view_model.dart'; GetIt getIt = GetIt.instance; @@ -121,6 +123,10 @@ class AppDependencies { getIt.registerLazySingleton(() => ContactUsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => HmgServicesRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), + ), + ); // ViewModels // Global/shared VMs → LazySingleton diff --git a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart index 42faafa..dc859a9 100644 --- a/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -1,165 +1,78 @@ -import 'dart:convert'; class ActivePrescriptionsResponseModel { - dynamic address; - int? appointmentNo; - dynamic clinic; - dynamic companyName; - int? days; - dynamic doctorName; - int? doseDailyQuantity; // doses per day + String? itemId; + String? itemDescription; + String? route; String? frequency; int? frequencyNumber; - dynamic image; - dynamic imageExtension; - dynamic imageSrcUrl; - String? imageString; - dynamic imageThumbUrl; - dynamic isCovered; - String? itemDescription; - int? itemId; + int? doseDailyQuantity; + int? days; + String? startDate; + String? endDate; String? orderDate; - int? patientId; - dynamic patientName; - dynamic phoneOffice1; - dynamic prescriptionQr; - dynamic prescriptionTimes; - dynamic productImage; - String? productImageBase64; String? productImageString; - int? projectId; - dynamic projectName; - dynamic remarks; - String? route; - String? sku; - int? scaleOffset; - String? startDate; - - // Added for reminder feature + bool isReminderOn; List selectedDoseTimes = []; - bool isReminderOn = false; // toggle status ActivePrescriptionsResponseModel({ - this.address, - this.appointmentNo, - this.clinic, - this.companyName, - this.days, - this.doctorName, - this.doseDailyQuantity, + this.itemId, + this.itemDescription, + this.route, this.frequency, this.frequencyNumber, - this.image, - this.imageExtension, - this.imageSrcUrl, - this.imageString, - this.imageThumbUrl, - this.isCovered, - this.itemDescription, - this.itemId, + this.doseDailyQuantity, + this.days, + this.startDate, + this.endDate, this.orderDate, - this.patientId, - this.patientName, - this.phoneOffice1, - this.prescriptionQr, - this.prescriptionTimes, - this.productImage, - this.productImageBase64, this.productImageString, - this.projectId, - this.projectName, - this.remarks, - this.route, - this.sku, - this.scaleOffset, - this.startDate, - - // ✅ Default values for new fields (won’t break API) - List? selectedDoseTimes, this.isReminderOn = false, - }) : selectedDoseTimes = selectedDoseTimes ?? []; - - factory ActivePrescriptionsResponseModel.fromRawJson(String str) => - ActivePrescriptionsResponseModel.fromJson(json.decode(str)); - - String toRawJson() => json.encode(toJson()); - - factory ActivePrescriptionsResponseModel.fromJson(Map json) => - ActivePrescriptionsResponseModel( - address: json["Address"], - appointmentNo: json["AppointmentNo"], - clinic: json["Clinic"], - companyName: json["CompanyName"], - days: json["Days"], - doctorName: json["DoctorName"], - doseDailyQuantity: json["DoseDailyQuantity"], - frequency: json["Frequency"], - frequencyNumber: json["FrequencyNumber"], - image: json["Image"], - imageExtension: json["ImageExtension"], - imageSrcUrl: json["ImageSRCUrl"], - imageString: json["ImageString"], - imageThumbUrl: json["ImageThumbUrl"], - isCovered: json["IsCovered"], - itemDescription: json["ItemDescription"], - itemId: json["ItemID"], - orderDate: json["OrderDate"], - patientId: json["PatientID"], - patientName: json["PatientName"], - phoneOffice1: json["PhoneOffice1"], - prescriptionQr: json["PrescriptionQR"], - prescriptionTimes: json["PrescriptionTimes"], - productImage: json["ProductImage"], - productImageBase64: json["ProductImageBase64"], - productImageString: json["ProductImageString"], - projectId: json["ProjectID"], - projectName: json["ProjectName"], - remarks: json["Remarks"], - route: json["Route"], - sku: json["SKU"], - scaleOffset: json["ScaleOffset"], - startDate: json["StartDate"], + List? selectedDoseTimes, + }) { + this.selectedDoseTimes = selectedDoseTimes ?? []; + } - // ✅ Ensure local reminder values are not overwritten by API - selectedDoseTimes: [], - isReminderOn: false, - ); + /// ========== JSON FROM ========== + factory ActivePrescriptionsResponseModel.fromJson(Map json) { + return ActivePrescriptionsResponseModel( + itemId: json["ItemID"]?.toString() ?? "", + itemDescription: json["ItemDescription"] ?? "", + route: json["Route"] ?? "", + frequency: json["Frequency"] ?? "", + frequencyNumber: json["FrequencyNumber"], + doseDailyQuantity: json["DoseDailyQuantity"] ?? 1, + days: json["Days"] ?? 0, + startDate: json["StartDate"] ?? "", + endDate: json["EndDate"] ?? "", + orderDate: json["OrderDate"] ?? "", + productImageString: json["ProductImageString"] ?? "", + isReminderOn: json["IsReminderOn"] == true, + selectedDoseTimes: + (json["SelectedDoseTimes"] as List?) + ?.map((e) => e?.toString()) + .toList() ?? + [], + ); + } - Map toJson() => { - "Address": address, - "AppointmentNo": appointmentNo, - "Clinic": clinic, - "CompanyName": companyName, - "Days": days, - "DoctorName": doctorName, - "DoseDailyQuantity": doseDailyQuantity, - "Frequency": frequency, - "FrequencyNumber": frequencyNumber, - "Image": image, - "ImageExtension": imageExtension, - "ImageSRCUrl": imageSrcUrl, - "ImageString": imageString, - "ImageThumbUrl": imageThumbUrl, - "IsCovered": isCovered, - "ItemDescription": itemDescription, - "ItemID": itemId, - "OrderDate": orderDate, - "PatientID": patientId, - "PatientName": patientName, - "PhoneOffice1": phoneOffice1, - "PrescriptionQR": prescriptionQr, - "PrescriptionTimes": prescriptionTimes, - "ProductImage": productImage, - "ProductImageBase64": productImageBase64, - "ProductImageString": productImageString, - "ProjectID": projectId, - "ProjectName": projectName, - "Remarks": remarks, - "Route": route, - "SKU": sku, - "ScaleOffset": scaleOffset, - "StartDate": startDate, - }; + /// ========== JSON TO ========== + Map toJson() { + return { + "ItemID": itemId, + "ItemDescription": itemDescription, + "Route": route, + "Frequency": frequency, + "FrequencyNumber": frequencyNumber, + "DoseDailyQuantity": doseDailyQuantity, + "Days": days, + "StartDate": startDate, + "EndDate": endDate, + "OrderDate": orderDate, + "ProductImageString": productImageString, + "IsReminderOn": isReminderOn, + "SelectedDoseTimes": selectedDoseTimes, + }; + } } diff --git a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart index d5180ae..6c24998 100644 --- a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart +++ b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart @@ -10,6 +10,7 @@ class HmgServicesComponentModel { Color bgColor; Color textColor; String route; + bool isExternalLink; HmgServicesComponentModel( this.action, @@ -21,5 +22,6 @@ class HmgServicesComponentModel { this.bgColor = Colors.white, this.textColor = Colors.black, this.route = '', + this.isExternalLink = false, }); } diff --git a/lib/features/terms_conditions/terms_conditions_repo.dart b/lib/features/terms_conditions/terms_conditions_repo.dart new file mode 100644 index 0000000..a5d3f95 --- /dev/null +++ b/lib/features/terms_conditions/terms_conditions_repo.dart @@ -0,0 +1,60 @@ +import 'package:dartz/dartz.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class TermsConditionsRepo { + Future> getTermsConditions(); +} + +class TermsConditionsRepoImp implements TermsConditionsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + TermsConditionsRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future> getTermsConditions() async { + Failure? failure; + String? html; + + try { + await apiClient.post( + ApiConsts.getTermsConditions, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType ?? ServerFailure(error.toString()); + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + + final content = response['UserAgreementContent']; + + if (content is String && content.isNotEmpty) { + html = content; + } else { + failure = DataParsingFailure( + 'UserAgreementContent is null or not String'); + } + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + } catch (e) { + failure = UnknownFailure(e.toString()); + } + + if (failure != null) return Left(failure!); + if (html == null || html!.isEmpty) { + return Left(ServerFailure('No terms and conditions returned')); + } + + return Right(html!); + } +} + diff --git a/lib/features/terms_conditions/terms_conditions_view_model.dart b/lib/features/terms_conditions/terms_conditions_view_model.dart new file mode 100644 index 0000000..5d67ae7 --- /dev/null +++ b/lib/features/terms_conditions/terms_conditions_view_model.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class TermsConditionsViewModel extends ChangeNotifier { + final TermsConditionsRepo termsConditionsRepo; + final ErrorHandlerService errorHandlerService; + + String? termsConditionsHtml; + bool isLoading = false; + + TermsConditionsViewModel({ + required this.termsConditionsRepo, + required this.errorHandlerService, + }); + + Future getTermsConditions({ + Function()? onSuccess, + Function(String)? onError, + }) async { + isLoading = true; + notifyListeners(); + + final result = await termsConditionsRepo.getTermsConditions(); + + result.fold( + (failure) async { + await errorHandlerService.handleError(failure: failure); + isLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message ?? 'Something went wrong'); + } + }, + (html) { + termsConditionsHtml = html; + isLoading = false; + notifyListeners(); + if (onSuccess != null) onSuccess(); + }, + ); + } +} + + diff --git a/lib/main.dart b/lib/main.dart index f127400..e9ceec7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,6 +38,7 @@ import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'core/utils/size_utils.dart'; +import 'features/terms_conditions/terms_conditions_view_model.dart'; import 'firebase_options.dart'; @pragma('vm:entry-point') @@ -146,9 +147,12 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ) + ), ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index d27b35d..d0720fb 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -547,7 +547,7 @@ class _ActiveMedicationPageState extends State { AppColors.textGreenColor, AppColors.infoColor, AppColors.labelColorYellow, - AppColors.purpleBg + AppColors.mainPurple ][doseIndex % 4]; final doseLabel = "${doseIndex + 1}${_getSuffix(doseIndex + 1)}"; diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index af576aa..bc63876 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_s import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:url_launcher/url_launcher.dart'; class ServicesPage extends StatelessWidget { ServicesPage({super.key}); @@ -41,8 +42,30 @@ class ServicesPage extends StatelessWidget { textColor: AppColors.blackColor, route: AppRoutes.homeHealthCarePage, ), + HmgServicesComponentModel( + 12, + "Latest News".needTranslation, + "".needTranslation, + AppAssets.news, + true, + bgColor: AppColors.bgGreenColor, + textColor: AppColors.blackColor, + route: "https://twitter.com/HMG", + isExternalLink: true, + ), + HmgServicesComponentModel( + 12, + "Monthly Reports".needTranslation, + "".needTranslation, + AppAssets.report_icon, + true, + bgColor: AppColors.bgGreenColor, + textColor: AppColors.blackColor, + route: AppRoutes.monthlyReports, + ), ]; + @override Widget build(BuildContext context) { return CollapsingListView( @@ -72,7 +95,7 @@ class ServicesPage extends StatelessWidget { return ServiceGridViewItem(hmgServices[index], index, false); }, ), - ) + ), ], ), ), diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 225bd96..3a0c211 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -5,6 +5,7 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_services_component_model.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:url_launcher/url_launcher.dart'; class ServiceGridViewItem extends StatelessWidget { final HmgServicesComponentModel hmgServiceComponentModel; @@ -12,12 +13,18 @@ class ServiceGridViewItem extends StatelessWidget { final bool isHomePage; final bool isLocked; - const ServiceGridViewItem(this.hmgServiceComponentModel, this.index, this.isHomePage, {super.key, this.isLocked = false}); + const ServiceGridViewItem( + this.hmgServiceComponentModel, this.index, this.isHomePage, + {super.key, this.isLocked = false}); @override Widget build(BuildContext context) { return InkWell( - onTap: () => getIt.get().pushPageRoute(hmgServiceComponentModel.route), + onTap: () => hmgServiceComponentModel.isExternalLink + ? _openLink(hmgServiceComponentModel.route) + : getIt + .get() + .pushPageRoute(hmgServiceComponentModel.route), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, @@ -47,4 +54,14 @@ class ServiceGridViewItem extends StatelessWidget { ], )); } + + Future _openLink(String link) async { + final Uri url = Uri.parse(link); + + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + throw "Could not launch $url"; + } + } } diff --git a/lib/presentation/monthly_reports/monthly_reports_page.dart b/lib/presentation/monthly_reports/monthly_reports_page.dart new file mode 100644 index 0000000..97cc0e3 --- /dev/null +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -0,0 +1,283 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.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/presentation/monthly_reports/user_agreement_page.dart'; + +import '../../generated/locale_keys.g.dart'; +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/input_widget.dart'; + +class MonthlyReportsPage extends StatefulWidget { + const MonthlyReportsPage({super.key}); + + @override + State createState() => _MonthlyReportsPageState(); +} + +class _MonthlyReportsPageState extends State { + bool isHealthSummaryEnabled = false; + bool isTermsAccepted = false; + + final TextEditingController emailController = TextEditingController(); + + @override + void dispose() { + emailController.dispose(); + super.dispose(); + } + + void _showError(String message) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + ), + ); + } + + void _onSavePressed() { + if (!isTermsAccepted) { + _showError("Please accept the terms and conditions".needTranslation); + return; + } + + final email = emailController.text.trim(); + if (email.isEmpty) { + _showError("Please enter your email".needTranslation); + return; + } + + setState(() { + isHealthSummaryEnabled = true; + }); + + // TODO: هنا حطي API/logic حق الحفظ + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Monthly Reports".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 16.h), + + Container( + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + height: 54.h, + alignment: Alignment.center, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: Colors.white, + borderRadius: (12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Patient Health Summary Report".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + _buildToggle(), + ], + ), + ), + + SizedBox(height: 16.h), + + TextInputWidget( + controller: emailController, + labelText: "Eamil*".needTranslation, + hintText: "email@email.com", + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + autoFocus: true, + keyboardType: TextInputType.emailAddress, + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h), + onChange: (value) { + setState(() {}); + }, + ).paddingOnly(top: 8.h, bottom: 8.h), + + Row( + children: [ + Text( + "To View The Terms and Conditions".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + InkWell( + child: Text( + "Click here".needTranslation, + style: TextStyle( + color: AppColors.errorColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const UserAgreementPage(), + ), + ); + }, + ), + ], + ), + + SizedBox(height: 12.h), + + GestureDetector( + onTap: () => setState(() => isTermsAccepted = !isTermsAccepted), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 24.h, + width: 24.h, + decoration: BoxDecoration( + color: isTermsAccepted + ? AppColors.textGreenColor + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isTermsAccepted + ? AppColors.lightGreenColor + : AppColors.greyColor, + width: 2.h, + ), + ), + child: isTermsAccepted + ? Icon(Icons.check, size: 16.f, color: Colors.white) + : null, + ), + SizedBox(width: 12.h), + Text( + "I agree to the terms and conditions".needTranslation, + style: context.dynamicTextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + color: const Color(0xFF2E3039), + ), + ), + ], + ), + ), + + SizedBox(height: 12.h), + + Text( + "This is monthly health summary report".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 10.f, + fontWeight: FontWeight.w600, + ), + ), + + SizedBox(height: 12.h), + + Image.asset('assets/images/jpg/report.jpg'), + + SizedBox(height: 16.h), + + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.successColor, + foregroundColor: AppColors.whiteColor, + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: _onSavePressed, + child: Text( + LocaleKeys.save.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.f, + ), + ), + ), + ), + ], + ), + ], + ), + ).paddingAll(16), + ); + } + + Widget _buildToggle() { + final value = isHealthSummaryEnabled; + + return AbsorbPointer( + absorbing: true, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 50.h, + height: 28.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: value + ? AppColors.lightGreenColor + : AppColors.greyColor.withOpacity(0.3), + ), + child: AnimatedAlign( + duration: const Duration(milliseconds: 200), + alignment: value ? Alignment.centerRight : Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(3), + child: Container( + width: 22.h, + height: 22.h, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: value + ? AppColors.textGreenColor + : AppColors.greyTextColor, + ), + ), + ), + ), + ), + ); + } +} + + diff --git a/lib/presentation/monthly_reports/user_agreement_page.dart b/lib/presentation/monthly_reports/user_agreement_page.dart new file mode 100644 index 0000000..f6379ad --- /dev/null +++ b/lib/presentation/monthly_reports/user_agreement_page.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_view_model.dart'; +import 'package:provider/provider.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; + +class UserAgreementPage extends StatefulWidget { + const UserAgreementPage({super.key}); + + @override + State createState() => _UserAgreementPageState(); +} + +class _UserAgreementPageState extends State { + late final WebViewController _webViewController; + bool _isLoading = true; + String? _errorMessage; + + @override + void initState() { + super.initState(); + + _webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onPageStarted: (_) { + setState(() { + _isLoading = true; + }); + }, + onPageFinished: (_) { + setState(() { + _isLoading = false; + }); + }, + onWebResourceError: (error) { + }, + ), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final vm = + Provider.of(context, listen: false); + + vm.getTermsConditions( + onSuccess: () { + final htmlString = vm.termsConditionsHtml ?? ''; + + if (htmlString.isNotEmpty) { + setState(() { + _errorMessage = null; + _isLoading = true; + }); + _webViewController.loadHtmlString(htmlString); + } else { + setState(() { + _isLoading = false; + _errorMessage = 'لا توجد شروط متاحة حالياً'.needTranslation; + }); + } + }, + onError: (msg) { + setState(() { + _isLoading = false; + _errorMessage = msg; + }); + }, + ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Stack( + children: [ + WebViewWidget(controller: _webViewController), + + if (_errorMessage != null) + Center( + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _errorMessage!, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index a0ee1e5..7330744 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -9,6 +9,8 @@ import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../presentation/monthly_reports/monthly_reports_page.dart'; + class AppRoutes { static const String initialRoute = '/initialRoute'; static const String loginScreen = '/loginScreen'; @@ -19,7 +21,7 @@ class AppRoutes { static const String eReferralPage = '/erReferralPage'; static const String comprehensiveCheckupPage = '/comprehensiveCheckupPage'; static const String homeHealthCarePage = '/homeHealthCarePage'; - + static const String monthlyReports = '/monthlyReportsPage'; static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -29,6 +31,7 @@ class AppRoutes { medicalFilePage: (context) => MedicalFilePage(), eReferralPage: (context) => EReferralPage(), comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), - homeHealthCarePage: (context) => HhcProceduresPage() + homeHealthCarePage: (context) => HhcProceduresPage(), + monthlyReports: (context) => MonthlyReportsPage() }; } diff --git a/pubspec.yaml b/pubspec.yaml index 3d6604c..0de3829 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,7 +79,7 @@ dependencies: path_provider: ^2.0.8 open_filex: ^4.7.0 flutter_swiper_view: ^1.1.8 - + webview_flutter: ^4.9.0 location: ^8.0.1 gms_check: ^1.0.4 huawei_location: ^6.14.2+301 From c9de23347a69da4ad2ae3bbcf559e94752cee5cc Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Mon, 15 Dec 2025 12:30:42 +0300 Subject: [PATCH 07/12] fix error --- lib/core/app_assets.dart | 15 ++++++++++++--- lib/core/dependencies.dart | 2 +- lib/core/utils/calender_utils_new.dart | 1 + .../ui_models/hmg_services_component_model.dart | 3 ++- lib/presentation/hmg_services/services_view.dart | 6 +++--- lib/routes/app_routes.dart | 4 ++-- lib/splashPage.dart | 1 + pubspec.yaml | 1 - 8 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 4e5c7b8..40add26 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -1,7 +1,6 @@ class AppAssets { static const String svgBasePath = 'assets/images/svg'; static const String pngBasePath = 'assets/images/png'; - static const String jpgBasePath = 'assets/images/jpg'; static const String hmg = '$svgBasePath/hmg.svg'; static const String arrow_back = '$svgBasePath/arrow-back.svg'; @@ -235,6 +234,17 @@ class AppAssets { static const String heart = '$svgBasePath/heart.svg'; static const String alertSquare = '$svgBasePath/alert-square.svg'; static const String arrowRight = '$svgBasePath/arrow-right.svg'; + static const String tickIcon = '$svgBasePath/tick.svg'; + + // Symptoms Checker + static const String calendarGrey = '$svgBasePath/calendar-grey.svg'; + static const String weightScale = '$svgBasePath/weight-scale.svg'; + static const String rulerIcon = '$svgBasePath/ruler.svg'; + static const String genderIcon = '$svgBasePath/gender.svg'; + static const String bodyIcon = '$svgBasePath/body_icon.svg'; + static const String rotateIcon = '$svgBasePath/rotate_icon.svg'; + static const String refreshIcon = '$svgBasePath/refresh.svg'; + static const String homeBorderedIcon = '$svgBasePath/home_bordered.svg'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; @@ -257,8 +267,6 @@ class AppAssets { static const String fullBodyFront = '$pngBasePath/full_body_front.png'; static const String fullBodyBack = '$pngBasePath/full_body_back.png'; - // JPGS // - static const String report = '$jpgBasePath/hmg_logo.jpg'; } class AppAnimations { @@ -280,3 +288,4 @@ class AppAnimations { static const String ambulanceAlert = '$lottieBasePath/ambulance_alert.json'; static const String rrtAmbulance = '$lottieBasePath/rrt_ambulance.json'; } + diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 674cd02..a6699c6 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -134,7 +134,7 @@ class AppDependencies { getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), - ), + ),); // ViewModels // Global/shared VMs → LazySingleton diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index 5a43d78..5e9e91b 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:device_calendar_plus/device_calendar_plus.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:jiffy/jiffy.dart' show Jiffy; +import 'package:manage_calendar_events/manage_calendar_events.dart' hide Calendar; class CalenderUtilsNew { final DeviceCalendar calender = DeviceCalendar.instance; diff --git a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart index 5e531c7..ebc9511 100644 --- a/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart +++ b/lib/features/hmg_services/models/ui_models/hmg_services_component_model.dart @@ -9,7 +9,8 @@ class HmgServicesComponentModel { bool isLogin; bool isLocked; Color textColor; - String route; + Color bgColor; + String? route; bool isExternalLink; Function? onTap; diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 237b4cd..59efb73 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -19,16 +19,16 @@ class ServiceGridViewItem extends StatelessWidget { const ServiceGridViewItem( this.hmgServiceComponentModel, this.index, this.isHomePage, - {super.key, this.isLocked = false}); + {super.key, this.isLocked = false, required this.isHealthToolIcon, this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: () => hmgServiceComponentModel.isExternalLink - ? _openLink(hmgServiceComponentModel.route) + ? _openLink(hmgServiceComponentModel.route!) : getIt .get() - .pushPageRoute(hmgServiceComponentModel.route), + .pushPageRoute(hmgServiceComponentModel.route!), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f32a003..81c5ee4 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -82,8 +82,8 @@ class AppRoutes { huaweiHealthExample: (context) => HuaweiHealthExample(), // - healthCalculatorsPage: (context) => HealthCalculatorsPage() - monthlyReports: (context) => MonthlyReportsPage() + healthCalculatorsPage: (context) => HealthCalculatorsPage(), + monthlyReports: (context) => MonthlyReportsPage() }; } diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 9aa16ee..2327ab5 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -35,6 +35,7 @@ import 'core/utils/local_notifications.dart'; import 'core/utils/push_notification_handler.dart'; import 'widgets/routes/custom_page_route.dart'; + class SplashPage extends StatefulWidget { @override _SplashScreenState createState() => _SplashScreenState(); diff --git a/pubspec.yaml b/pubspec.yaml index 9f0be5f..5590c9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,6 @@ dependencies: google_maps_flutter: ^2.13.1 flutter_zoom_videosdk: 2.1.10 dart_jsonwebtoken: ^3.2.0 - webview_flutter: ^4.9.0 dartz: ^0.10.1 equatable: ^2.0.7 google_api_availability: ^5.0.1 From 68f044de523a526e5f641b9eaa411dc25ebb9705 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 24 Dec 2025 09:54:39 +0300 Subject: [PATCH 08/12] added monthly report --- lib/core/dependencies.dart | 7 +- .../monthly_reports/monthly_reports_repo.dart | 96 +++++++++++++++++++ .../monthly_reports_view_model.dart | 33 +++++++ .../terms_conditions_repo.dart | 0 .../terms_conditions_view_model.dart | 2 +- lib/main.dart | 2 +- .../monthly_reports/monthly_reports_page.dart | 51 +++++++--- .../monthly_reports/user_agreement_page.dart | 8 +- lib/routes/app_routes.dart | 14 ++- 9 files changed, 192 insertions(+), 21 deletions(-) create mode 100644 lib/features/monthly_reports/monthly_reports_repo.dart create mode 100644 lib/features/monthly_reports/monthly_reports_view_model.dart rename lib/features/{terms_conditions => monthly_reports}/terms_conditions_repo.dart (100%) rename lib/features/{terms_conditions => monthly_reports}/terms_conditions_view_model.dart (92%) diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index a6699c6..8cc29df 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/location/location_repo.dart'; import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_repo.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/monthly_reports_repo.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; @@ -48,6 +49,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_v import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/monthly_reports/monthly_reports_page.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -62,8 +64,8 @@ import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../features/active_prescriptions/active_prescriptions_repo.dart'; -import '../features/terms_conditions/terms_conditions_repo.dart'; -import '../features/terms_conditions/terms_conditions_view_model.dart'; +import '../features/monthly_reports/terms_conditions_repo.dart'; +import '../features/monthly_reports/terms_conditions_view_model.dart'; GetIt getIt = GetIt.instance; @@ -135,6 +137,7 @@ class AppDependencies { getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), ),); + getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels // Global/shared VMs → LazySingleton diff --git a/lib/features/monthly_reports/monthly_reports_repo.dart b/lib/features/monthly_reports/monthly_reports_repo.dart new file mode 100644 index 0000000..4ace6ec --- /dev/null +++ b/lib/features/monthly_reports/monthly_reports_repo.dart @@ -0,0 +1,96 @@ +import 'package:dartz/dartz.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + +abstract class MonthlyReportsRepo { + Future>> saveMonthlyReport({ + String? email, + }); +} + +class MonthlyReportsRepoImp implements MonthlyReportsRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + MonthlyReportsRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future>> saveMonthlyReport({ + String? email, + }) async { + try { + Failure? failure; + + GenericApiModel? reportApiResponse; + + await apiClient.post( + ApiConsts.getMonthlyReports, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + reportApiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (reportApiResponse == null) return Left(ServerFailure("Unknown error")); + + if ((reportApiResponse!.messageStatus ?? 0) != 1) { + return Right(reportApiResponse!); + } + + GenericApiModel? emailApiResponse; + + final Map emailRequest = {}; + + if (email != null && email.trim().isNotEmpty) { + emailRequest["Email"] = email.trim(); + } + + await apiClient.post( + ApiConsts.updatePatientEmail, + body: emailRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + emailApiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (emailApiResponse == null) return Left(ServerFailure("Unknown error")); + + return Right(emailApiResponse!); + } catch (e) { + loggerService.logError("MonthlyReportsRepo.saveMonthlyReport error: $e"); + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/monthly_reports/monthly_reports_view_model.dart b/lib/features/monthly_reports/monthly_reports_view_model.dart new file mode 100644 index 0000000..4fd82da --- /dev/null +++ b/lib/features/monthly_reports/monthly_reports_view_model.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'monthly_reports_repo.dart'; +class MonthlyReportsViewModel extends ChangeNotifier { + final MonthlyReportsRepo monthlyReportsRepo; + final ErrorHandlerService errorHandlerService; + + bool isLoading = false; + + MonthlyReportsViewModel({ + required this.monthlyReportsRepo, + required this.errorHandlerService, + }); + + Future saveMonthlyReport({String? email}) async { + isLoading = true; + notifyListeners(); + + final result = await monthlyReportsRepo.saveMonthlyReport(email: email); + + final success = result.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + return false; + }, + (apiResponse) => (apiResponse.messageStatus ?? 0) == 1, + ); + + isLoading = false; + notifyListeners(); + return success; + } +} diff --git a/lib/features/terms_conditions/terms_conditions_repo.dart b/lib/features/monthly_reports/terms_conditions_repo.dart similarity index 100% rename from lib/features/terms_conditions/terms_conditions_repo.dart rename to lib/features/monthly_reports/terms_conditions_repo.dart diff --git a/lib/features/terms_conditions/terms_conditions_view_model.dart b/lib/features/monthly_reports/terms_conditions_view_model.dart similarity index 92% rename from lib/features/terms_conditions/terms_conditions_view_model.dart rename to lib/features/monthly_reports/terms_conditions_view_model.dart index 5d67ae7..bd70b87 100644 --- a/lib/features/terms_conditions/terms_conditions_view_model.dart +++ b/lib/features/monthly_reports/terms_conditions_view_model.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_repo.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/terms_conditions_repo.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class TermsConditionsViewModel extends ChangeNotifier { diff --git a/lib/main.dart b/lib/main.dart index 801a54c..714053f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,7 +43,7 @@ import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'core/utils/size_utils.dart'; -import 'features/terms_conditions/terms_conditions_view_model.dart'; +import 'features/monthly_reports/terms_conditions_view_model.dart'; import 'firebase_options.dart'; @pragma('vm:entry-point') diff --git a/lib/presentation/monthly_reports/monthly_reports_page.dart b/lib/presentation/monthly_reports/monthly_reports_page.dart index 97cc0e3..78c3f30 100644 --- a/lib/presentation/monthly_reports/monthly_reports_page.dart +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -3,12 +3,15 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.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/monthly_reports/monthly_reports_view_model.dart'; import 'package:hmg_patient_app_new/presentation/monthly_reports/user_agreement_page.dart'; +import 'package:provider/provider.dart'; import '../../generated/locale_keys.g.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/input_widget.dart'; +import '../../widgets/loader/bottomsheet_loader.dart'; class MonthlyReportsPage extends StatefulWidget { const MonthlyReportsPage({super.key}); @@ -39,7 +42,25 @@ class _MonthlyReportsPageState extends State { ); } - void _onSavePressed() { + void _showSuccessSnackBar() { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Successfully updated".needTranslation, + style: const TextStyle( + color: AppColors.whiteColor, + fontWeight: FontWeight.w600, + ), + ), + behavior: SnackBarBehavior.floating, + backgroundColor: AppColors.textGreenColor, + duration: const Duration(seconds: 2), + ), + ); + } + + Future _onSavePressed() async { if (!isTermsAccepted) { _showError("Please accept the terms and conditions".needTranslation); return; @@ -51,11 +72,18 @@ class _MonthlyReportsPageState extends State { return; } - setState(() { - isHealthSummaryEnabled = true; - }); + final vm = context.read(); - // TODO: هنا حطي API/logic حق الحفظ + // LoaderBottomSheet.showLoader(); + final ok = await vm.saveMonthlyReport(email: email); + // LoaderBottomSheet.hideLoader(); + + if (ok) { + setState(() => isHealthSummaryEnabled = true); + _showSuccessSnackBar(); + } else { + _showError("Failed to update".needTranslation); + } } @override @@ -87,7 +115,7 @@ class _MonthlyReportsPageState extends State { height: 54.h, alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: (12.r), ), child: Row( @@ -139,7 +167,7 @@ class _MonthlyReportsPageState extends State { child: Text( "Click here".needTranslation, style: TextStyle( - color: AppColors.errorColor, + color: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w600, ), @@ -179,7 +207,7 @@ class _MonthlyReportsPageState extends State { ), ), child: isTermsAccepted - ? Icon(Icons.check, size: 16.f, color: Colors.white) + ? Icon(Icons.check, size: 16.f, color: AppColors.whiteColor,) : null, ), SizedBox(width: 12.h), @@ -188,7 +216,7 @@ class _MonthlyReportsPageState extends State { style: context.dynamicTextStyle( fontSize: 12.f, fontWeight: FontWeight.w500, - color: const Color(0xFF2E3039), + color: AppColors.textColor, ), ), ], @@ -198,7 +226,8 @@ class _MonthlyReportsPageState extends State { SizedBox(height: 12.h), Text( - "This is monthly health summary report".needTranslation, + "This monthly Health Summary Report reflects the health indicators and analysis results of the latest visits. Please note that this will be sent automatically from the system and it's not considered as an official report so no medical decisions should be taken based on it" + .needTranslation, style: TextStyle( color: AppColors.textColor, fontSize: 10.f, @@ -279,5 +308,3 @@ class _MonthlyReportsPageState extends State { ); } } - - diff --git a/lib/presentation/monthly_reports/user_agreement_page.dart b/lib/presentation/monthly_reports/user_agreement_page.dart index f6379ad..73ea564 100644 --- a/lib/presentation/monthly_reports/user_agreement_page.dart +++ b/lib/presentation/monthly_reports/user_agreement_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; -import 'package:hmg_patient_app_new/features/terms_conditions/terms_conditions_view_model.dart'; +import 'package:hmg_patient_app_new/features/monthly_reports/terms_conditions_view_model.dart'; import 'package:provider/provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -93,14 +93,14 @@ class _UserAgreementPageState extends State { margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(8), ), child: Text( _errorMessage!, textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.red, + style: TextStyle( + color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, ), ), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 81c5ee4..11c6c00 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -23,7 +23,12 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_sele import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../core/dependencies.dart'; +import '../features/monthly_reports/monthly_reports_repo.dart'; +import '../features/monthly_reports/monthly_reports_view_model.dart'; import '../presentation/monthly_reports/monthly_reports_page.dart'; +import '../services/error_handler_service.dart'; +import 'package:provider/provider.dart'; class AppRoutes { static const String initialRoute = '/initialRoute'; @@ -83,7 +88,14 @@ class AppRoutes { // healthCalculatorsPage: (context) => HealthCalculatorsPage(), - monthlyReports: (context) => MonthlyReportsPage() + // monthlyReports: (context) => MonthlyReportsPage() + monthlyReports: (context) => ChangeNotifierProvider( + create: (_) => MonthlyReportsViewModel( + monthlyReportsRepo: getIt(), + errorHandlerService: getIt(), + ), + child: const MonthlyReportsPage(), + ), }; } From 7a03242396d96e49544c26f765a86128076c8395 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 24 Dec 2025 11:26:19 +0300 Subject: [PATCH 09/12] added monthly report --- lib/core/api_consts.dart | 4 +++- lib/generated/locale_keys.g.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 778f751..390e448 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -703,7 +703,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -829,6 +829,8 @@ class ApiConsts { static final String acceptAndRejectFamilyFile = 'Services/Authentication.svc/REST/Update_FileStatus'; static final String getActivePrescriptionsDetails = 'Services/Patients.svc/Rest/GetActivePrescriptionReportByPatientID'; static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; + static final String getMonthlyReports = 'Services/Patients.svc/Rest/UpdatePateintHealthSummaryReport'; + static final String updatePatientEmail = 'Services/Patients.svc/Rest/UpdatePateintEmail'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index d76422b..44d671f 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -476,7 +476,7 @@ abstract class LocaleKeys { static const shareReview = 'shareReview'; static const review = 'review'; static const viewMedicalFile = 'viewMedicalFile'; - static const viewAllServices = 'viewAllServices'; + static String get viewAllServices => 'viewAllServices'; static const medicalFile = 'medicalFile'; static const verified = 'verified'; static const checkup = 'checkup'; From a53602dc8cc40d962f6e4b50ed4d5627fd844a6e Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 24 Dec 2025 11:26:32 +0300 Subject: [PATCH 10/12] added monthly report --- android/app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 6c4ac3d..328e8fc 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -19,5 +19,5 @@ Geofence requests happened too frequently. - sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg + From 62bb667cb8edf7acec48982eaa021400bab47655 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Wed, 31 Dec 2025 11:38:51 +0300 Subject: [PATCH 11/12] added parking part --- lib/core/dependencies.dart | 2 +- .../hmg_services/services_page.dart | 121 ++++++++------- .../monthly_reports/monthly_reports_page.dart | 2 +- lib/presentation/parking/paking_page.dart | 118 +++++++++++++++ lib/presentation/parking/parking_slot.dart | 142 ++++++++++++++++++ lib/routes/app_routes.dart | 2 +- 6 files changed, 333 insertions(+), 54 deletions(-) create mode 100644 lib/presentation/parking/paking_page.dart create mode 100644 lib/presentation/parking/parking_slot.dart diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 2f45f37..555ce29 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -48,7 +48,7 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_r import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart'; +// import 'package:hmg_patient_app_new/presentation/health_calculators/health_calculator_view_model.dart'; import 'package:hmg_patient_app_new/presentation/monthly_reports/monthly_reports_page.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index bfd9f30..7ae7916 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -21,6 +21,7 @@ import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; + import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -31,6 +32,7 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import '../../core/dependencies.dart' show getIt; @@ -108,27 +110,27 @@ class ServicesPage extends StatelessWidget { true, route: AppRoutes.homeHealthCarePage, ), - HmgServicesComponentModel( - 12, - "Latest News".needTranslation, - "".needTranslation, - AppAssets.news, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: "https://twitter.com/HMG", - isExternalLink: true, - ), - HmgServicesComponentModel( - 12, - "Monthly Reports".needTranslation, - "".needTranslation, - AppAssets.report_icon, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: AppRoutes.monthlyReports, - ), + // HmgServicesComponentModel( + // 12, + // "Latest News".needTranslation, + // "".needTranslation, + // AppAssets.news, + // true, + // bgColor: AppColors.bgGreenColor, + // textColor: AppColors.blackColor, + // route: "https://twitter.com/HMG", + // isExternalLink: true, + // ), + // HmgServicesComponentModel( + // 12, + // "Monthly Reports".needTranslation, + // "".needTranslation, + // AppAssets.report_icon, + // true, + // bgColor: AppColors.bgGreenColor, + // textColor: AppColors.blackColor, + // route: AppRoutes.monthlyReports, + // ), ]; late final List hmgHealthToolServices = [ @@ -169,17 +171,17 @@ class ServicesPage extends StatelessWidget { route: AppRoutes.smartWatches, // route: AppRoutes.huaweiHealthExample, ), - HmgServicesComponentModel( - 12, - "Latest News".needTranslation, - "".needTranslation, - AppAssets.news, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: "https://twitter.com/HMG", - isExternalLink: true, - ), + // HmgServicesComponentModel( + // 12, + // "Latest News".needTranslation, + // "".needTranslation, + // AppAssets.news, + // true, + // bgColor: AppColors.bgGreenColor, + // textColor: AppColors.blackColor, + // route: "https://twitter.com/HMG", + // isExternalLink: true, + // ), HmgServicesComponentModel( 12, "Monthly Reports".needTranslation, @@ -460,25 +462,42 @@ class ServicesPage extends StatelessWidget { ), SizedBox(width: 16.w), Expanded( - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.car_parking_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) - ], + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ParkingPage(), + ), + ); + }, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.car_parking_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ).onPress(() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ParkingPage(), + ), + ); + }), ), ), ), diff --git a/lib/presentation/monthly_reports/monthly_reports_page.dart b/lib/presentation/monthly_reports/monthly_reports_page.dart index 78c3f30..d1a4d0c 100644 --- a/lib/presentation/monthly_reports/monthly_reports_page.dart +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -82,7 +82,7 @@ class _MonthlyReportsPageState extends State { setState(() => isHealthSummaryEnabled = true); _showSuccessSnackBar(); } else { - _showError("Failed to update".needTranslation); + // _showError("Failed to update".needTranslation); } } diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart new file mode 100644 index 0000000..ce9b6ab --- /dev/null +++ b/lib/presentation/parking/paking_page.dart @@ -0,0 +1,118 @@ + + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.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/presentation/parking/parking_slot.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/routes/custom_page_route.dart'; + +class ParkingPage extends StatefulWidget { + const ParkingPage({super.key}); + + @override + State createState() => _ParkingPageState(); +} + +class _ParkingPageState extends State { + String? scannedCode; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Parking".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600)), + Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + // margin: EdgeInsets.all(10), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Text( + "Dr. Sulaiman Al Habib hospital are conduction a test for the emerging corona" + " virus and issuing travel certificates 24/7 in a short time and with high accuracy." + " Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches " + "to conduct a corona test within few minutes. Dr. Sulaiman Al Habib hospital are conduction" + " a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. " + "Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes.", + style: TextStyle( + color: AppColors.textColor, + fontSize: 12, height: 1.4, fontWeight: FontWeight.w500), + ), + ), + ).paddingOnly( top: 16, bottom: 16), + + ], + ), + ), + ), + + /// Bottom button + Container + ( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryRedColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: ParkingSlot(), + ) ); + }, + child: Text( + "Read Barcodes", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/parking/parking_slot.dart b/lib/presentation/parking/parking_slot.dart new file mode 100644 index 0000000..094dcb7 --- /dev/null +++ b/lib/presentation/parking/parking_slot.dart @@ -0,0 +1,142 @@ + + + + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../../theme/colors.dart'; +import '../../widgets/appbar/app_bar_widget.dart'; +import '../../widgets/chip/app_custom_chip_widget.dart'; + +class ParkingSlot extends StatefulWidget { + const ParkingSlot({super.key}); + + @override + State createState() => _ParkingSlotState(); +} + +class _ParkingSlotState extends State { + String? scannedCode; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: LayoutBuilder( + builder: (context, constraints) { + final maxW = constraints.maxWidth; + final contentW = maxW > 600 ? 600.0 : maxW; // حد أقصى للتابلت + + return Align( + alignment: Alignment.topCenter, + child: SizedBox( + width: contentW, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Parking Slot Details", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + SizedBox(height: 16.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + AppCustomChipWidget(labelText: "Slot: B-24".needTranslation), + AppCustomChipWidget(labelText: "Basement: Zone B".needTranslation), + AppCustomChipWidget(labelText: "Date: 16 Dec 2025".needTranslation), + AppCustomChipWidget(labelText: "Parked Since: 10:32 AM".needTranslation), + ], + ), + ], + ), + ), + ), + + SizedBox(height: 24.h), + + SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryRedColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () {}, + child: Text( + "Get Direction", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + + const Spacer(), + SizedBox( + width: double.infinity, + height: 48.h, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + side: BorderSide(color: AppColors.primaryRedColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () { + // Reset direction logic + }, + child: Text( + "Reset Direction", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryRedColor, + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + + ); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 970d2b9..db38e78 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -89,7 +89,7 @@ class AppRoutes { huaweiHealthExample: (context) => HuaweiHealthExample(), // - healthCalculatorsPage: (context) => HealthCalculatorsPage(), + // healthCalculatorsPage: (context) => HealthCalculatorsPage(), // monthlyReports: (context) => MonthlyReportsPage() monthlyReports: (context) => ChangeNotifierProvider( create: (_) => MonthlyReportsViewModel( From 8ae81d2b248f4508c951e0920f8e2e82fb07a187 Mon Sep 17 00:00:00 2001 From: "Fatimah.Alshammari" Date: Thu, 8 Jan 2026 10:37:59 +0300 Subject: [PATCH 12/12] fixed parking qr --- lib/core/api_consts.dart | 5 +- lib/core/dependencies.dart | 19 ++ .../models/qr_parking_response_model.dart | 183 ++++++++++++++++++ lib/features/qr_parking/qr_parking_repo.dart | 74 +++++++ .../qr_parking/qr_parking_view_model.dart | 144 ++++++++++++++ .../hmg_services/services_page.dart | 109 ++++------- .../medical_file/medical_file_page.dart | 4 +- lib/presentation/parking/paking_page.dart | 95 +++++---- lib/presentation/parking/parking_slot.dart | 177 +++++++++++++---- lib/routes/app_routes.dart | 14 +- 10 files changed, 670 insertions(+), 154 deletions(-) create mode 100644 lib/features/qr_parking/models/qr_parking_response_model.dart create mode 100644 lib/features/qr_parking/qr_parking_repo.dart create mode 100644 lib/features/qr_parking/qr_parking_view_model.dart diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 66b9b7e..b0fb2a8 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -207,7 +207,7 @@ var GET_APPOINTMENT_DETAILS_BY_NO = 'Services/MobileNotifications.svc/REST/GetAp var NEW_RATE_APPOINTMENT_URL = "Services/Doctors.svc/REST/AppointmentsRating_InsertAppointmentRate"; var NEW_RATE_DOCTOR_URL = "Services/Doctors.svc/REST/DoctorsRating_InsertDoctorRate"; -var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; +//var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; //URL to get clinic list var GET_CLINICS_LIST_URL = "Services/lists.svc/REST/GetClinicCentralized"; @@ -700,7 +700,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -828,6 +828,7 @@ class ApiConsts { static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; static final String getMonthlyReports = 'Services/Patients.svc/Rest/UpdatePateintHealthSummaryReport'; static final String updatePatientEmail = 'Services/Patients.svc/Rest/UpdatePateintEmail'; + static final String getQrParkingDetails = 'Services/SWP.svc/REST/GetQRParkingByID'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 555ce29..872fe69 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -41,6 +41,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; @@ -63,6 +64,7 @@ import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart'; import '../features/active_prescriptions/active_prescriptions_repo.dart'; @@ -140,6 +142,15 @@ class AppDependencies { getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), ),); getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => QrParkingRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + // ViewModels // Global/shared VMs → LazySingleton @@ -245,6 +256,14 @@ class AppDependencies { activePrescriptionsRepo: getIt() ), ); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + // Screen-specific VMs → Factory // getIt.registerFactory( diff --git a/lib/features/qr_parking/models/qr_parking_response_model.dart b/lib/features/qr_parking/models/qr_parking_response_model.dart new file mode 100644 index 0000000..2e90da1 --- /dev/null +++ b/lib/features/qr_parking/models/qr_parking_response_model.dart @@ -0,0 +1,183 @@ + + +class QrParkingResponseModel { + dynamic totalRecords; + dynamic nRowID; + int? qRParkingID; + String? description; + String? descriptionN; + dynamic qRCompare; + dynamic qRValue; + String? imagePath; + bool? isActive; + int? parkingID; + int? branchID; + int? companyID; + int? buildingID; + int? rowID; + int? gateID; + int? floorID; + dynamic imagePath1; + int? createdBy; + String? createdOn; + dynamic editedBy; + dynamic editedOn; + String? parkingDescription; + String? parkingDescriptionN; + String? gateDescription; + String? gateDescriptionN; + String? branchDescription; + String? branchDescriptionN; + String? companyDescription; + String? companyDescriptionN; + String? rowDescription; + String? rowDescriptionN; + String? floorDescription; + String? floorDescriptionN; + String? buildingDescription; + String? buildingDescriptionN; + String? qRParkingCode; + String? parkingCode; + double? latitude; + double? longitude; + String? qRImageStr; + + QrParkingResponseModel({ + this.totalRecords, + this.nRowID, + this.qRParkingID, + this.description, + this.descriptionN, + this.qRCompare, + this.qRValue, + this.imagePath, + this.isActive, + this.parkingID, + this.branchID, + this.companyID, + this.buildingID, + this.rowID, + this.gateID, + this.floorID, + this.imagePath1, + this.createdBy, + this.createdOn, + this.editedBy, + this.editedOn, + this.parkingDescription, + this.parkingDescriptionN, + this.gateDescription, + this.gateDescriptionN, + this.branchDescription, + this.branchDescriptionN, + this.companyDescription, + this.companyDescriptionN, + this.rowDescription, + this.rowDescriptionN, + this.floorDescription, + this.floorDescriptionN, + this.buildingDescription, + this.buildingDescriptionN, + this.qRParkingCode, + this.parkingCode, + this.latitude, + this.longitude, + this.qRImageStr, + }); + + QrParkingResponseModel.fromJson(Map json) { + totalRecords = json['TotalRecords']; + nRowID = json['nRowID']; + qRParkingID = json['QRParkingID']; + description = json['Description']; + descriptionN = json['DescriptionN']; + qRCompare = json['QRCompare']; + qRValue = json['QRValue']; + imagePath = json['ImagePath']; + isActive = json['IsActive']; + parkingID = json['ParkingID']; + branchID = json['BranchID']; + companyID = json['CompanyID']; + buildingID = json['BuildingID']; + rowID = json['RowID']; + gateID = json['GateID']; + floorID = json['FloorID']; + imagePath1 = json['ImagePath1']; + createdBy = json['CreatedBy']; + createdOn = json['CreatedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + parkingDescription = json['ParkingDescription']; + parkingDescriptionN = json['ParkingDescriptionN']; + gateDescription = json['GateDescription']; + gateDescriptionN = json['GateDescriptionN']; + branchDescription = json['BranchDescription']; + branchDescriptionN = json['BranchDescriptionN']; + companyDescription = json['CompanyDescription']; + companyDescriptionN = json['CompanyDescriptionN']; + rowDescription = json['RowDescription']; + rowDescriptionN = json['RowDescriptionN']; + floorDescription = json['FloorDescription']; + floorDescriptionN = json['FloorDescriptionN']; + buildingDescription = json['BuildingDescription']; + buildingDescriptionN = json['BuildingDescriptionN']; + qRParkingCode = json['QRParkingCode']; + parkingCode = json['ParkingCode']; + latitude = _toDouble(json['Latitude']); + longitude = _toDouble(json['Longitude']); + qRImageStr = json['QRImageStr']; + } + + Map toJson() { + final Map data = {}; + data['TotalRecords'] = totalRecords; + data['nRowID'] = nRowID; + data['QRParkingID'] = qRParkingID; + data['Description'] = description; + data['DescriptionN'] = descriptionN; + data['QRCompare'] = qRCompare; + data['QRValue'] = qRValue; + data['ImagePath'] = imagePath; + data['IsActive'] = isActive; + data['ParkingID'] = parkingID; + data['BranchID'] = branchID; + data['CompanyID'] = companyID; + data['BuildingID'] = buildingID; + data['RowID'] = rowID; + data['GateID'] = gateID; + data['FloorID'] = floorID; + data['ImagePath1'] = imagePath1; + data['CreatedBy'] = createdBy; + data['CreatedOn'] = createdOn; + data['EditedBy'] = editedBy; + data['EditedOn'] = editedOn; + data['ParkingDescription'] = parkingDescription; + data['ParkingDescriptionN'] = parkingDescriptionN; + data['GateDescription'] = gateDescription; + data['GateDescriptionN'] = gateDescriptionN; + data['BranchDescription'] = branchDescription; + data['BranchDescriptionN'] = branchDescriptionN; + data['CompanyDescription'] = companyDescription; + data['CompanyDescriptionN'] = companyDescriptionN; + data['RowDescription'] = rowDescription; + data['RowDescriptionN'] = rowDescriptionN; + data['FloorDescription'] = floorDescription; + data['FloorDescriptionN'] = floorDescriptionN; + data['BuildingDescription'] = buildingDescription; + data['BuildingDescriptionN'] = buildingDescriptionN; + data['QRParkingCode'] = qRParkingCode; + data['ParkingCode'] = parkingCode; + data['Latitude'] = latitude; + data['Longitude'] = longitude; + data['QRImageStr'] = qRImageStr; + return data; + } + + static double? _toDouble(dynamic v) { + if (v == null) return null; + if (v is double) return v; + if (v is int) return v.toDouble(); + return double.tryParse(v.toString()); + } +} + diff --git a/lib/features/qr_parking/qr_parking_repo.dart b/lib/features/qr_parking/qr_parking_repo.dart new file mode 100644 index 0000000..1ec905f --- /dev/null +++ b/lib/features/qr_parking/qr_parking_repo.dart @@ -0,0 +1,74 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + + +abstract class QrParkingRepo { + Future>>> + getQrParking({ + required int qrParkingId, + }); +} + +class QrParkingRepoImp implements QrParkingRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + QrParkingRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future>>> + getQrParking({required int qrParkingId}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getQrParkingDetails, // GetQRParkingByID + body: {'QRParkingID': qrParkingId}, + onFailure: (error, statusCode, + {messageStatus, failureType}) { + failure = failureType ?? + StatusCodeFailure("$error ($statusCode)"); + }, + onSuccess: (response, statusCode, + {messageStatus, errorMessage}) { + final list = + (response['List_SWP_QRParkingModel'] as List?) ?? []; + + final res = list + .map((e) => QrParkingResponseModel.fromJson( + Map.from(e), + )) + .toList(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: res, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) { + return Left(ServerFailure("Unknown error")); + } + + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} + diff --git a/lib/features/qr_parking/qr_parking_view_model.dart b/lib/features/qr_parking/qr_parking_view_model.dart new file mode 100644 index 0000000..b0b688f --- /dev/null +++ b/lib/features/qr_parking/qr_parking_view_model.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; + +import '../../services/cache_service.dart'; +import '../../services/error_handler_service.dart'; +import 'models/qr_parking_response_model.dart'; + + +class QrParkingViewModel extends ChangeNotifier { + final QrParkingRepo qrParkingRepo; + final ErrorHandlerService errorHandlerService; + final CacheService cacheService; + String IS_GO_TO_PARKING = 'IS_GO_TO_PARKING'; + + bool isLoading = false; + String? error; + + bool isSavePark = false; + QrParkingResponseModel? qrParkingModel; + List qrParkingList = []; + + QrParkingViewModel({ + required this.qrParkingRepo, + required this.errorHandlerService, + required this.cacheService, + }); + + + Future scanAndGetParking() async { + try { + error = null; + isLoading = true; + notifyListeners(); + + final result = await BarcodeScanner.scan(); + + if (result.type != ResultType.Barcode) { + isLoading = false; + notifyListeners(); + return null; + } + + final raw = result.rawContent.trim(); + if (raw.isEmpty) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final qrParkingId = _extractQrParkingId(raw); + if (qrParkingId == null) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final apiResult = + await qrParkingRepo.getQrParking(qrParkingId: qrParkingId); + + final model = apiResult.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + error = failure.toString(); + return null; + }, + (apiResponse) { + qrParkingList = apiResponse.data ?? []; + if (qrParkingList.isNotEmpty) { + return qrParkingList.first; + } + error = "Invalid Qr Code"; + return null; + }, + ); + + if (model != null) { + qrParkingModel = model; + isSavePark = true; + + await cacheService.saveObject( + key: IS_GO_TO_PARKING, + value: model.toJson(), + ); + } + + isLoading = false; + notifyListeners(); + return model; + } catch (e) { + error = "Scan error"; + isLoading = false; + notifyListeners(); + return null; + } + } + + /// Load saved parking + Future getIsSaveParking() async { + isLoading = true; + notifyListeners(); + + final parking = + await cacheService.getObject(key: IS_GO_TO_PARKING); + + if (parking != null) { + isSavePark = true; + qrParkingModel = QrParkingResponseModel.fromJson( + Map.from(parking), + ); + } else { + isSavePark = false; + qrParkingModel = null; + } + + isLoading = false; + notifyListeners(); + } + + /// Reset parking + Future clearParking() async { + await cacheService.remove(key: IS_GO_TO_PARKING); + isSavePark = false; + qrParkingModel = null; + notifyListeners(); + } + + int? _extractQrParkingId(String raw) { + try { + if (raw.startsWith("{")) { + final data = jsonDecode(raw); + return int.tryParse(data['QRParkingID'].toString()); + } + return int.tryParse(raw); + } catch (_) { + return null; + } + } +} + + diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 87a0e0e..a3d5d76 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -21,7 +21,7 @@ import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; - +import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -31,10 +31,9 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import '../../core/dependencies.dart' show getIt; +import '../../features/qr_parking/qr_parking_view_model.dart'; class ServicesPage extends StatelessWidget { ServicesPage({super.key}); @@ -89,30 +88,18 @@ class ServicesPage extends StatelessWidget { route: null, onTap: () async { LoaderBottomSheet.showLoader(loadingText: "Fetching Data..."); await bloodDonationViewModel.getRegionSelectedClinics(onSuccess: (val) async { - // await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { + await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { LoaderBottomSheet.hideLoader(); Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( CustomPageRoute( page: BloodDonationPage(), ), ); - // }, onError: (err) { - // LoaderBottomSheet.hideLoader(); - // }); + }); }, onError: (err) { LoaderBottomSheet.hideLoader(); }); }), - HmgServicesComponentModel( - 11, - "Covid 19 Test".needTranslation, - "".needTranslation, - AppAssets.covid19icon, - bgColor: AppColors.covid29Color, - true, - route: AppRoutes.covid19Test, - ) - // HmgServicesComponentModel( // 3, // "Home Health Care".needTranslation, @@ -176,30 +163,8 @@ class ServicesPage extends StatelessWidget { route: AppRoutes.smartWatches, // route: AppRoutes.huaweiHealthExample, ), - // HmgServicesComponentModel( - // 12, - // "Latest News".needTranslation, - // "".needTranslation, - // AppAssets.news, - // true, - // bgColor: AppColors.bgGreenColor, - // textColor: AppColors.blackColor, - // route: "https://twitter.com/HMG", - // isExternalLink: true, - // ), - HmgServicesComponentModel( - 12, - "Monthly Reports".needTranslation, - "".needTranslation, - AppAssets.report_icon, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: AppRoutes.monthlyReports, - ), ]; - @override Widget build(BuildContext context) { bloodDonationViewModel = Provider.of(context); @@ -467,43 +432,37 @@ class ServicesPage extends StatelessWidget { ), SizedBox(width: 16.w), Expanded( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ParkingPage(), - ), - ); - }, - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.car_parking_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) - ], - ).onPress(() { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ParkingPage(), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.car_parking_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ).onPress(() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), ), - ); - }), - ), + ), + ); + + }), ), ), ), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 52dc9ff..80a45ac 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -1069,7 +1069,9 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.monthly_reports_icon, isLargeText: true, iconSize: 36.h, - ), + ).onPress(() { + Navigator.pushNamed(context, AppRoutes.monthlyReports); + }), MedicalFileCard( label: "Medical Reports".needTranslation, textColor: AppColors.blackColor, diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart index ce9b6ab..cd1e8bc 100644 --- a/lib/presentation/parking/paking_page.dart +++ b/lib/presentation/parking/paking_page.dart @@ -1,16 +1,18 @@ - import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/app_export.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/presentation/parking/parking_slot.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:provider/provider.dart'; +import '../../features/qr_parking/qr_parking_view_model.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/routes/custom_page_route.dart'; + class ParkingPage extends StatefulWidget { const ParkingPage({super.key}); @@ -19,10 +21,29 @@ class ParkingPage extends StatefulWidget { } class _ParkingPageState extends State { - String? scannedCode; + Future _readQR(BuildContext context) async { + final vm = context.read(); + + final model = await vm.scanAndGetParking(); + + if (model == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(vm.error ?? "Invalid Qr Code")), + ); + return; + } + + Navigator.of(context).push( + CustomPageRoute( + page: ParkingSlot(model: model), + ), + ); + } @override Widget build(BuildContext context) { + final vm = context.watch(); // عشان loading + return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( @@ -38,22 +59,24 @@ class _ParkingPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Parking".needTranslation, - style: TextStyle( - color: AppColors.textColor, - fontSize: 27.f, - fontWeight: FontWeight.w600)), - Container( - decoration: RoundedRectangleBorder() - .toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - // margin: EdgeInsets.all(10), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Text( + Text( + "Parking".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600, + ), + ), + Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Text( "Dr. Sulaiman Al Habib hospital are conduction a test for the emerging corona" " virus and issuing travel certificates 24/7 in a short time and with high accuracy." " Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches " @@ -62,19 +85,20 @@ class _ParkingPageState extends State { "Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes.", style: TextStyle( color: AppColors.textColor, - fontSize: 12, height: 1.4, fontWeight: FontWeight.w500), - ), - ), - ).paddingOnly( top: 16, bottom: 16), - + fontSize: 12, + height: 1.4, + fontWeight: FontWeight.w500, + ), + ), + ), + ).paddingOnly(top: 16, bottom: 16), ], ), ), ), /// Bottom button - Container - ( + Container( decoration: RoundedRectangleBorder() .toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -93,13 +117,17 @@ class _ParkingPageState extends State { borderRadius: BorderRadius.circular(10), ), ), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: ParkingSlot(), - ) ); - }, - child: Text( + onPressed: vm.isLoading ? null : () => _readQR(context), + child: vm.isLoading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( "Read Barcodes", style: TextStyle( fontSize: 18, @@ -116,3 +144,4 @@ class _ParkingPageState extends State { ); } } + diff --git a/lib/presentation/parking/parking_slot.dart b/lib/presentation/parking/parking_slot.dart index 094dcb7..013bb6f 100644 --- a/lib/presentation/parking/parking_slot.dart +++ b/lib/presentation/parking/parking_slot.dart @@ -1,26 +1,106 @@ - - import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; +import '../../features/qr_parking/qr_parking_view_model.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:provider/provider.dart'; + class ParkingSlot extends StatefulWidget { - const ParkingSlot({super.key}); + final QrParkingResponseModel model; + + const ParkingSlot({ + super.key, + required this.model, + }); @override State createState() => _ParkingSlotState(); } class _ParkingSlotState extends State { - String? scannedCode; + + void _openDirection() { + final lat = widget.model.latitude; + final lng = widget.model.longitude; + + final valid = lat != null && + lng != null && + !(lat == 0.0 && lng == 0.0) && + lat >= -90 && lat <= 90 && + lng >= -180 && lng <= 180; + + if (!valid) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Parking location not available")), + ); + return; + } + + MapsLauncher.launchCoordinates(lat, lng); + } + + Future _resetDirection() async { + final vm = context.read(); + await vm.clearParking(); + Navigator.of(context).popUntil((route) => route.isFirst); + } + + DateTime? _parseDotNetDate(String? value) { + if (value == null || value.isEmpty) return null; + + final regExp = RegExp(r'Date\((\d+)([+-]\d+)?\)'); + final match = regExp.firstMatch(value); + if (match == null) return null; + + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds == null) return null; + + return DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true) + .toLocal(); + } + + + String _formatPrettyDate(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + + final day = date.day; + final month = months[date.month - 1]; + final year = date.year; + + return "$day $month $year"; + } + + + String _formatPrettyTime(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + int hour = date.hour; + final minute = date.minute.toString().padLeft(2, '0'); + + final isPM = hour >= 12; + final period = isPM ? 'PM' : 'AM'; + + hour = hour % 12; + if (hour == 0) hour = 12; + + return "${hour.toString().padLeft(2, '0')}:$minute $period"; + } @override Widget build(BuildContext context) { @@ -34,7 +114,7 @@ class _ParkingSlotState extends State { body: LayoutBuilder( builder: (context, constraints) { final maxW = constraints.maxWidth; - final contentW = maxW > 600 ? 600.0 : maxW; // حد أقصى للتابلت + final contentW = maxW > 600 ? 600.0 : maxW; return Align( alignment: Alignment.topCenter, @@ -45,9 +125,11 @@ class _ParkingSlotState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Container( width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true, @@ -58,7 +140,7 @@ class _ParkingSlotState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Parking Slot Details", + "Parking Slot Details".needTranslation, style: TextStyle( fontSize: 16.f, fontWeight: FontWeight.w600, @@ -70,10 +152,26 @@ class _ParkingSlotState extends State { spacing: 4, runSpacing: 4, children: [ - AppCustomChipWidget(labelText: "Slot: B-24".needTranslation), - AppCustomChipWidget(labelText: "Basement: Zone B".needTranslation), - AppCustomChipWidget(labelText: "Date: 16 Dec 2025".needTranslation), - AppCustomChipWidget(labelText: "Parked Since: 10:32 AM".needTranslation), + AppCustomChipWidget( + labelText: + "Slot: ${widget.model.qRParkingCode ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Basement: ${widget.model.floorDescription ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Date: ${_formatPrettyDate(widget.model.createdOn)}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Parked Since: ${_formatPrettyTime(widget.model.createdOn)}" + .needTranslation, + ), ], ), ], @@ -93,42 +191,40 @@ class _ParkingSlotState extends State { borderRadius: BorderRadius.circular(10), ), ), - onPressed: () {}, + onPressed: _openDirection, child: Text( - "Get Direction", + "Get Direction".needTranslation, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.whiteColor, ), ), ), ), - const Spacer(), - SizedBox( - width: double.infinity, - height: 48.h, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - side: BorderSide(color: AppColors.primaryRedColor), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - onPressed: () { - // Reset direction logic - }, - child: Text( - "Reset Direction", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.primaryRedColor, - ), - ), - ), - ), + // const Spacer(), + // SizedBox( + // width: double.infinity, + // height: 48.h, + // child: OutlinedButton( + // style: OutlinedButton.styleFrom( + // side: BorderSide(color: AppColors.primaryRedColor), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // ), + // onPressed: _resetDirection, + // child: Text( + // "Reset Direction".needTranslation, + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.w600, + // color: AppColors.primaryRedColor, + // ), + // ), + // ), + // ), ], ), ), @@ -136,7 +232,8 @@ class _ParkingSlotState extends State { ); }, ), - ); } } + + diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index dd38180..5b18bce 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -24,12 +24,14 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_sele import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/covid19test/covid19_landing_page.dart'; import '../core/dependencies.dart'; import '../features/monthly_reports/monthly_reports_repo.dart'; import '../features/monthly_reports/monthly_reports_view_model.dart'; import '../presentation/monthly_reports/monthly_reports_page.dart'; +import '../presentation/parking/paking_page.dart'; import '../services/error_handler_service.dart'; import 'package:provider/provider.dart'; @@ -65,6 +67,7 @@ class AppRoutes { static const String userInfoSelection = '/userInfoSelection'; static const String userInfoFlowManager = '/userInfoFlowManager'; static const String monthlyReports = '/monthlyReportsPage'; + static const String qrParking = '/qrParkingPage'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -92,7 +95,6 @@ class AppRoutes { covid19Test: (context) => Covid19LandingPage(), // // healthCalculatorsPage: (context) => HealthCalculatorsPage(), - // monthlyReports: (context) => MonthlyReportsPage() monthlyReports: (context) => ChangeNotifierProvider( create: (_) => MonthlyReportsViewModel( monthlyReportsRepo: getIt(), @@ -102,6 +104,12 @@ class AppRoutes { ), healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), - healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter) - }; + healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), + qrParking: (context) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), + ), + + + }; }