diff --git a/assets/images/jpg/report.jpg b/assets/images/jpg/report.jpg new file mode 100644 index 0000000..5846cd5 Binary files /dev/null and b/assets/images/jpg/report.jpg differ diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 07f9ee5..fe0b4d5 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"; @@ -785,6 +785,11 @@ 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 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/app_assets.dart b/lib/core/app_assets.dart index c470bc2..d378c78 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -349,3 +349,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 582b795..e8d3071 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -10,6 +10,8 @@ import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_repo. import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.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/common/common_repo.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_repo.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; @@ -44,6 +46,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'; @@ -55,6 +58,7 @@ import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.da import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; +import 'package:hmg_patient_app_new/presentation/monthly_reports/monthly_reports_page.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; @@ -68,8 +72,13 @@ 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'; +import '../features/monthly_reports/terms_conditions_repo.dart'; +import '../features/monthly_reports/terms_conditions_view_model.dart'; + GetIt getIt = GetIt.instance; class AppDependencies { @@ -147,6 +156,19 @@ class AppDependencies { getIt.registerLazySingleton(() => MyInvoicesRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => HealthTrackersRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => MonthlyReportRepoImp(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(), + ),); + 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 @@ -274,5 +296,19 @@ class AppDependencies { navServices: getIt(), )); getIt.registerLazySingleton(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton( + () => ActivePrescriptionsViewModel( + errorHandlerService: getIt(), + activePrescriptionsRepo: getIt() + ), + ); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + } } diff --git a/lib/core/utils/calendar_utils.dart b/lib/core/utils/calendar_utils.dart new file mode 100644 index 0000000..8c0db18 --- /dev/null +++ b/lib/core/utils/calendar_utils.dart @@ -0,0 +1,316 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:device_calendar/device_calendar.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_reminder_view.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; +import 'package:hmg_patient_app_new/services/permission_service.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:manage_calendar_events/manage_calendar_events.dart' as ios; +import 'package:permission_handler/permission_handler.dart'; +import 'package:timezone/data/latest.dart' as tzl; + +final DeviceCalendarPlugin deviceCalendarPlugin = DeviceCalendarPlugin(); +final ios.CalendarPlugin _myPlugin = ios.CalendarPlugin(); + +class CalendarUtils { + static Completer? _completer; + + dynamic get writableCalendars => calendars.firstWhere((c) => !c.isReadOnly!); + dynamic calendars; + + CalendarUtils._(this.calendars); + + // static Future getInstance() async { + // if (_completer == null) { + // _completer = Completer(); + // print(_completer!.isCompleted); + // try { + // final dynamic calendarsResult; + // if (Platform.isIOS) { + // calendarsResult = await _myPlugin.getCalendars(); + // if (!_completer!.isCompleted) { + // _completer?.complete(CalendarUtils._(await calendarsResult!)); + // } + // } else { + // calendarsResult = await deviceCalendarPlugin.retrieveCalendars(); + // if (!_completer!.isCompleted) { + // _completer?.complete(CalendarUtils._(await calendarsResult.data!)); + // } + // } + // } on Exception catch (e) { + // if (!_completer!.isCompleted) { + // _completer!.completeError(e); + // } + // } + // } + // return _completer!.future; + // } + + static Future getInstance() async { + tzl.initializeTimeZones(); + if (_completer != null) { + return _completer!.future; + } + _completer = Completer(); + try { + final dynamic calendarsResult; + if (Platform.isIOS) { + calendarsResult = await _myPlugin.getCalendars(); + _completer!.complete(CalendarUtils._(calendarsResult)); + } else { + calendarsResult = await deviceCalendarPlugin.retrieveCalendars(); + _completer!.complete(CalendarUtils._(calendarsResult.data)); + } + } catch (e) { + _completer!.completeError(e); + } + + return _completer!.future; + } + + Future createOrUpdateEvents({List? scheduleList, String? title, String? description, List? scheduleDateTime, List? daysOfWeek}) async { + tzl.initializeTimeZones(); + List events = []; + Location _currentLocation; + if (DateTime.now().timeZoneName == "+04") + _currentLocation = getLocation('Asia/Dubai'); + else + _currentLocation = getLocation('Asia/Riyadh'); + + scheduleDateTime!.forEach((element) { + RecurrenceRule recurrenceRule = RecurrenceRule( + // RecurrenceFrequency.Daily, + // daysOfWeek: daysOfWeek, + // endDate: element, + until: element, frequency: Frequency.daily, + ); + //added byAamir Tz Time + Event event = Event(writableCalendars!.id, + recurrenceRule: recurrenceRule, + start: TZDateTime.from(element, _currentLocation), + end: TZDateTime.from(element.add(Duration(minutes: 30)), _currentLocation), + title: title, + description: description); + events.add(event); + }); + + events.forEach((element) { + deviceCalendarPlugin.createOrUpdateEvent(element); + }); + } + + Future createOrUpdateEvent({required String title, required String description, required String location, DateTime? scheduleDateTime, String? eventId}) async { + RecurrenceRule recurrenceRule = RecurrenceRule( + // RecurrenceFrequency.Daily, + // daysOfWeek: daysOfWeek, + // endDate: scheduleDateTime, + until: scheduleDateTime, frequency: Frequency.daily, + ); + + Location _currentLocation; + // if (DateTime.now().timeZoneName == "+04") + // _currentLocation = getLocation('Asia/Dubai'); + // else + _currentLocation = getLocation('Asia/Riyadh'); + + TZDateTime scheduleDateTimeUTZ = TZDateTime.from(scheduleDateTime!, _currentLocation); + + print("writableCalendars-name: " + writableCalendars.name); + print("writableCalendars-Id: " + writableCalendars.id); + print("writableCalendarsToString: " + writableCalendars.toString()); + print("writableCalendarsToString: " + writableCalendars!.id!); + Event event = Event( + writableCalendars!.id, + start: scheduleDateTimeUTZ, + end: scheduleDateTimeUTZ.add(Duration(minutes: 30)), + title: title, + description: description, + ); + + ios.CalendarEvent iosCalEvent = + ios.CalendarEvent(location: location, startDate: scheduleDateTimeUTZ, endDate: scheduleDateTimeUTZ.add(Duration(minutes: 30)), title: title, description: description, isAllDay: false); + + if (Platform.isAndroid) { + Result result = await deviceCalendarPlugin.hasPermissions(); + print(result); + await deviceCalendarPlugin.createOrUpdateEvent(event).catchError((e) { + print("catchError " + e.toString()); + }).whenComplete(() { + print("whenComplete Calender ID " + eventId!); + }); + } else { + await _myPlugin.createEvent(calendarId: writableCalendars.id!, event: iosCalEvent).catchError((e) { + print("catchError " + e.toString()); + }).whenComplete(() { + print("whenComplete Calender ID iOS " + eventId!); + }); + } + } + + deleteEvent(String _calendarId, String _eventId) async { + if (Platform.isIOS) { + await _myPlugin.deleteEvent(calendarId: _calendarId, eventId: _eventId); + } else { + await deviceCalendarPlugin.deleteEvent(_calendarId, _eventId); + } + } + + Future retrieveEvents( + String calendarId, + RetrieveEventsParams retrieveEventsParams, + ) async { + if (Platform.isIOS) { + return await _myPlugin.getEvents(calendarId: calendarId); + } else { + return await deviceCalendarPlugin.retrieveEvents(calendarId, retrieveEventsParams); + } + } + + Future createCalendar( + String calendarName, { + Color? calendarColor, + String? localAccountName, + }) async { + return await deviceCalendarPlugin.createCalendar(calendarName, calendarColor: calendarColor, localAccountName: localAccountName); + } +} + +Future> requestPermissions() async { + var permissionResults = [Permission.calendarFullAccess].request(); + return permissionResults; +} + +showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, + {required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false}) async { + if (Platform.isAndroid) { + if (await PermissionService.isCalendarPermissionEnabled()) { + _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + } else { + // Utils.showPermissionConsentDialog(context, TranslationBase.of(context).calendarPermission, () async { + // if (await Permission.calendarFullAccess.request().isGranted) { + // _showReminderDialog(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + // onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + // } + // }); + } + } else { + if (await Permission.calendarWriteOnly.request().isGranted) { + if (await Permission.calendarFullAccess.request().isGranted) { + _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, + onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); + } + } + } +} + +Future _showReminderBottomSheet(BuildContext providedContext, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, + {required Function onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool? isMultiAllowed}) async { + showCommonBottomSheetWithoutHeight(providedContext, title: "Set the timer of reminder".needTranslation, child: PrescriptionReminderView( + setReminder: (int value) async { + if (!isMultiAllowed!) { + if (onMultiDateSuccess == null) { + CalendarUtils calendarUtils = await CalendarUtils.getInstance(); + await calendarUtils.createOrUpdateEvent( + title: title ?? "You have appointment with Dr. ".needTranslation + doctorName, + description: description ?? "At " + appoDateFormatted + " " + appoTimeFormatted, + scheduleDateTime: dateTime, + eventId: eventId, + location: ''); + onSuccess(); + } + } else { + onMultiDateSuccess!(value); + } + }, + ), callBackFunc: () {}, isFullScreen: false); +} + +setCalender(BuildContext context, + {required String eventId, required int selectedMinutes, int? frequencyNumber, required int days, required String orderDate, required String itemDescriptionN, required String route}) async { + DateTime actualDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0); + frequencyNumber ??= 2; //Some time frequency number is null so by default will be 2 + + int remainingDays = days - (Jiffy.parseFromDateTime(DateTime.now()).diff(Jiffy.parseFromDateTime(DateUtil.convertStringToDate(orderDate)), unit: Unit.day) as int); + if (remainingDays.isNegative) { + getIt.get().showErrorBottomSheet(message: "Prescription date has been already passed you can not add a reminder for this prescription."); + return; + } + CalendarUtils calendarUtils = await CalendarUtils.getInstance(); + + try { + for (int i = 0; i < remainingDays; i++) { + //event for number of days. + for (int j = 0; j < frequencyNumber; j++) { + // event for number of times per day. + if (j != 0) { + actualDate.add(new Duration(hours: 8)); // 8 hours addition for daily dose. + } + //Time subtraction from actual reminder time. like before 30, or 1 hour. + + actualDate = Jiffy.parseFromDateTime(actualDate).subtract(minutes: selectedMinutes).dateTime; + + calendarUtils.createOrUpdateEvent( + title: "$itemDescriptionN} Prescription Reminder", + description: "$itemDescriptionN $frequencyNumber $route ", + scheduleDateTime: actualDate, + 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; + } + } catch (ex) { + getIt.get().showErrorBottomSheet(message: "catch:$ex"); + } +} + +Future checkAndRemove(hasReminder, {bool delete = false, String itemDescriptionN = ""}) async { + final ios.CalendarPlugin _myPlugin = ios.CalendarPlugin(); + CalendarUtils calendarUtils = await CalendarUtils.getInstance(); + DateTime startEventsDate = Jiffy.parseFromDateTime(DateTime.now()).subtract(days: 30).dateTime; + DateTime endEventsDate = Jiffy.parseFromDateTime(DateTime.now()).add(days: 120).dateTime; + RetrieveEventsParams params = RetrieveEventsParams(startDate: startEventsDate, endDate: endEventsDate); + + if (calendarUtils.calendars != null) { + if (Platform.isAndroid) { + await processEvents(calendarUtils.calendars, calendarUtils, params, delete, itemDescriptionN, hasReminder); + } else { + List? iosCalendars = await _myPlugin.getCalendars(); + if (iosCalendars != null) { + await processEvents(iosCalendars.map((cal) => Calendar(id: cal.id, name: cal.name, accountName: cal.accountName)).toList(), calendarUtils, params, delete, itemDescriptionN, hasReminder); + } + } + } +} + +Future processEvents(List calendars, calendarUtils, params, delete, String itemDescriptionN, hasReminder) async { + for (var calendar in calendars) { + Result> events = await calendarUtils.retrieveEvents(calendar.id!, params); + for (var event in events.data!) { + if (event.title!.contains(itemDescriptionN)) { + if (delete) { + await calendarUtils.deleteEvent(calendar, event); + // AppToast.showSuccessToast(message: TranslationBase.of(context).reminderCancelSuccess); + hasReminder = false; + } else { + hasReminder = false; + // setState(() { + // hasReminder = true; + // }); + } + } + } + } +} 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/active_prescriptions/active_prescriptions_repo.dart b/lib/features/active_prescriptions/active_prescriptions_repo.dart new file mode 100644 index 0000000..437f364 --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_repo.dart @@ -0,0 +1,65 @@ + + +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 { + var list = response['List_ActiveGetPrescriptionReportByPatientID']; + var res = list + .map( + (item) => ActivePrescriptionsResponseModel.fromJson(item)) + .toList(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + // data: response, + data: res + ); + return 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())); + } + } + +} \ 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..e4da04d --- /dev/null +++ b/lib/features/active_prescriptions/active_prescriptions_view_model.dart @@ -0,0 +1,101 @@ + +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 { + late ActivePrescriptionsRepo activePrescriptionsRepo; + late ErrorHandlerService errorHandlerService; + List activePrescriptionsDetailsList = []; + + ActivePrescriptionsViewModel({ + required this.activePrescriptionsRepo, + required this.errorHandlerService, + }); + + 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 == 1) { + activePrescriptionsDetailsList = apiResponse.data ?? []; + notifyListeners(); + 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(); + } + + List generateMedicationDays(ActivePrescriptionsResponseModel med) { + final start = parseDate(med.startDate); + final duration = med.days ?? 0; + 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; + } + else if (f.contains("once a week")) { + intervalDays = 7; + } + else if (f.contains("every 3 days")) { + intervalDays = 3; + } + else if (f.contains("every other day")) { + intervalDays = 2; + } + + List result = []; + for (int offset = 0; offset < duration; offset += intervalDays) { + result.add(start.add(Duration(days: offset))); + } + + return result; + } + + bool sameYMD(DateTime a, DateTime b) => + a.year == b.year && a.month == b.month && a.day == b.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, 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 new file mode 100644 index 0000000..dc859a9 --- /dev/null +++ b/lib/features/active_prescriptions/models/active_prescriptions_response_model.dart @@ -0,0 +1,78 @@ + +class ActivePrescriptionsResponseModel { + String? itemId; + String? itemDescription; + String? route; + String? frequency; + int? frequencyNumber; + int? doseDailyQuantity; + int? days; + String? startDate; + String? endDate; + String? orderDate; + String? productImageString; + bool isReminderOn; + List selectedDoseTimes = []; + + ActivePrescriptionsResponseModel({ + this.itemId, + this.itemDescription, + this.route, + this.frequency, + this.frequencyNumber, + this.doseDailyQuantity, + this.days, + this.startDate, + this.endDate, + this.orderDate, + this.productImageString, + this.isReminderOn = false, + List? selectedDoseTimes, + }) { + this.selectedDoseTimes = selectedDoseTimes ?? []; + } + + /// ========== 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() ?? + [], + ); + } + + + + /// ========== 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 7a92198..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 @@ -8,8 +8,10 @@ class HmgServicesComponentModel { Color? iconColor; bool isLogin; bool isLocked; + Color textColor; Color bgColor; String? route; + bool isExternalLink; Function? onTap; HmgServicesComponentModel( @@ -23,6 +25,8 @@ class HmgServicesComponentModel { this.bgColor = Colors.white, this.iconColor = Colors.white, this.route, - this.onTap + this.onTap, + this.textColor = Colors.black, + this.isExternalLink = false, }); } 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/monthly_reports/terms_conditions_repo.dart b/lib/features/monthly_reports/terms_conditions_repo.dart new file mode 100644 index 0000000..a5d3f95 --- /dev/null +++ b/lib/features/monthly_reports/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/monthly_reports/terms_conditions_view_model.dart b/lib/features/monthly_reports/terms_conditions_view_model.dart new file mode 100644 index 0000000..bd70b87 --- /dev/null +++ b/lib/features/monthly_reports/terms_conditions_view_model.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.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 { + 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/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/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index ea0bcb8..f550c81 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -475,7 +475,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'; diff --git a/lib/main.dart b/lib/main.dart index 23bb3e2..e730e07 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ 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/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; @@ -46,6 +47,7 @@ import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'core/utils/size_utils.dart'; +import 'features/monthly_reports/terms_conditions_view_model.dart'; import 'firebase_options.dart'; @pragma('vm:entry-point') @@ -181,6 +183,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 new file mode 100644 index 0000000..d0720fb --- /dev/null +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -0,0 +1,1035 @@ + +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: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 '../../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 '../../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 { + const ActiveMedicationPage({super.key}); + + @override + State createState() => _ActiveMedicationPageState(); +} + +class _ActiveMedicationPageState extends State { + late DateTime currentDate; + late DateTime selectedDate; + List selectedDayMeds = []; + ActivePrescriptionsViewModel? activePreVM; + + + 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() { + super.initState(); + currentDate = DateTime.now(); + selectedDate = currentDate; + + WidgetsBinding.instance.addPostFrameCallback((_) async { + activePreVM = + Provider.of(context, listen: false); + LoaderBottomSheet.showLoader(); + await activePreVM!.getActiveMedications( + onSuccess: (_) async { + LoaderBottomSheet.hideLoader(); + + final todayMeds = + activePreVM!.getMedsForSelectedDay(selectedDate); + setState(() => selectedDayMeds = todayMeds); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await loadSavedReminders(); + }); + }, + onError: (_) { + LoaderBottomSheet.hideLoader(); + }, + ); + + activePreVM!.addListener(() { + if (!mounted) return; + 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))); + + @override + Widget build(BuildContext context) { + final days = getUpcomingDays(); + return Scaffold( + backgroundColor: AppColors.scaffoldBgColor, + appBar: CustomAppBar( + onBackPressed: () => Navigator.of(context).pop(), + onLanguageChanged: (_) {}, + hideLogoAndLang: true, + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Active Medications".needTranslation, + style: TextStyle( + color: AppColors.textColor, + 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), + ); + }, + ), + ), + SizedBox(height: 20.h), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "${selectedDate.day}", + style: TextStyle( + color: AppColors.textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + 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, + ), + ), + ), + ), + 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 = _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: [ + 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(med), + ], + ).paddingAll(16), + ), + const Divider( + color: AppColors.greyColor), + _buildButtons(), + ], + ), + ); + }, + ) + : 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 ?? "", + 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), + ), + ), + ]), + 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(ActivePrescriptionsResponseModel med) { + final medKey = _buildMedKey(med); + final value = medReminderStatus[medKey] ?? false; + + return GestureDetector( + onTap: () async { + 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), + 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) { + 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 showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 520.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bottomSheetBgColor, + 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, + children: [ + Text( + "Reminders".needTranslation, + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.w600, + 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: 20.h), + Expanded( + child: ListView.builder( + itemCount: doses, + itemBuilder: (context, doseIndex) { + final badgeColor = [ + AppColors.textGreenColor, + AppColors.infoColor, + AppColors.labelColorYellow, + AppColors.mainPurple + ][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, doseIndex); + }, + child: Container( + 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: 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, + children: [ + Expanded( + child: Text( + "Set reminder for $doseLabel dose", + style: TextStyle( + color: AppColors.textColor, + fontWeight: FontWeight.bold, + fontSize: 16.f, + ), + ), + ), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + height: 24.h, + width: 24.h, + iconColor: + AppColors.textColor, + ), + ], + ), + SizedBox(height: 4.h), + Text( + time, + style: TextStyle( + fontSize: 12.f, + color: AppColors.greyTextColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + + void showTimePickerSheet( + ActivePrescriptionsResponseModel med, int doseIndex) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => Container( + width: double.infinity, + height: 460.h, + decoration: const BoxDecoration( + color: AppColors.bottomSheetBgColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: ReminderTimerDialog( + med: med, + frequencyNumber: _getDosesCount(med), + doseIndex: doseIndex, + 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(() {}); + }, + ), + ), + ); + } + + 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( + width: 57.h, + height: 65.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: isSelected + ? AppColors.secondaryLightRedBorderColor + : Colors.transparent, + border: Border.all( + 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" : 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)) + ]), + ), + ), + ); + } + + 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 { + 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 { + 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 + ]; + + 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: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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), + Wrap( + spacing: 8, + runSpacing: 8, + 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), + ), + padding: const 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(height: 15.h), + if (showPicker) + SizedBox( + height: 100.h, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: false, + initialDateTime: DateTime( + 2024, + 1, + 1, + selectedTime.hour, + selectedTime.minute, + ), + onDateTimeChanged: (newTime) { + setState(() { + _selectedTime = null; + selectedTime = TimeOfDay(hour: newTime.hour, minute: newTime.minute); + bigTimeText = selectedTime.format(context).split(" ")[0]; + }); + }, + ), + ), + 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), + ), + ), + 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; + 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, + ), + ), + ), + ), + ], + ), + ], + ).paddingAll(16), + ), + ); + } + + 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 (_) { + return TimeOfDay.now(); + } + } +} + + + + + + diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 4d74bc7..e49a621 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -25,6 +25,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'; @@ -36,6 +37,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../../core/dependencies.dart' show getIt; +import '../../features/qr_parking/qr_parking_view_model.dart'; class ServicesPage extends StatelessWidget { ServicesPage({super.key}); @@ -527,7 +529,18 @@ class ServicesPage extends StatelessWidget { 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/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 3f0c44e..59efb73 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/extensions/widget_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:hmg_patient_app_new/theme/colors.dart'; +import 'package:url_launcher/url_launcher.dart'; class ServiceGridViewItem extends StatelessWidget { final HmgServicesComponentModel hmgServiceComponentModel; @@ -16,18 +17,18 @@ class ServiceGridViewItem extends StatelessWidget { final bool isHealthToolIcon; final Function? onTap; - const ServiceGridViewItem(this.hmgServiceComponentModel, this.index, this.isHomePage, {super.key, this.isLocked = false, this.onTap, this.isHealthToolIcon = false}); + const ServiceGridViewItem( + this.hmgServiceComponentModel, this.index, this.isHomePage, + {super.key, this.isLocked = false, required this.isHealthToolIcon, this.onTap}); @override Widget build(BuildContext context) { return InkWell( - onTap: () { - hmgServiceComponentModel.route != null - ? getIt.get().pushPageRoute(hmgServiceComponentModel.route!) - : hmgServiceComponentModel.onTap != null - ? hmgServiceComponentModel.onTap!() - : null; - }, + onTap: () => hmgServiceComponentModel.isExternalLink + ? _openLink(hmgServiceComponentModel.route!) + : getIt + .get() + .pushPageRoute(hmgServiceComponentModel.route!), child: Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, @@ -58,4 +59,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/home/landing_page.dart b/lib/presentation/home/landing_page.dart index df03d84..6d7a91c 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -59,6 +59,8 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/routes/spring_page_route_builder.dart'; import 'package:provider/provider.dart'; +import '../active_medication/active_medication_page.dart'; + class LandingPage extends StatefulWidget { const LandingPage({super.key}); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index e4ae130..c6ff85f 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/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/hmg_services/hmg_services_view_model.dart'; @@ -32,6 +33,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_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/allergies/allergies_list_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'; @@ -75,6 +77,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/shimmer/common_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'; @@ -98,6 +101,7 @@ class _MedicalFilePageState extends State { late MonthlyReportViewModel monthlyReportViewModel; final CacheService cacheService = GetIt.instance(); + late ActivePrescriptionsViewModel activePrescriptionsViewModel; int currentIndex = 0; 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..d1a4d0c --- /dev/null +++ b/lib/presentation/monthly_reports/monthly_reports_page.dart @@ -0,0 +1,310 @@ +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/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}); + + @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 _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; + } + + final email = emailController.text.trim(); + if (email.isEmpty) { + _showError("Please enter your email".needTranslation); + return; + } + + final vm = context.read(); + + // LoaderBottomSheet.showLoader(); + final ok = await vm.saveMonthlyReport(email: email); + // LoaderBottomSheet.hideLoader(); + + if (ok) { + setState(() => isHealthSummaryEnabled = true); + _showSuccessSnackBar(); + } else { + // _showError("Failed to update".needTranslation); + } + } + + @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: AppColors.whiteColor, + 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.primaryRedColor, + 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: AppColors.whiteColor,) + : null, + ), + SizedBox(width: 12.h), + Text( + "I agree to the terms and conditions".needTranslation, + style: context.dynamicTextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + color: AppColors.textColor, + ), + ), + ], + ), + ), + + SizedBox(height: 12.h), + + Text( + "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, + 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..73ea564 --- /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/monthly_reports/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: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _errorMessage!, + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.primaryRedColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart new file mode 100644 index 0000000..cd1e8bc --- /dev/null +++ b/lib/presentation/parking/paking_page.dart @@ -0,0 +1,147 @@ + +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: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}); + + @override + State createState() => _ParkingPageState(); +} + +class _ParkingPageState extends State { + 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( + 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, + ), + 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: 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, + 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..013bb6f --- /dev/null +++ b/lib/presentation/parking/parking_slot.dart @@ -0,0 +1,239 @@ + + +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/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 { + final QrParkingResponseModel model; + + const ParkingSlot({ + super.key, + required this.model, + }); + + @override + State createState() => _ParkingSlotState(); +} + +class _ParkingSlotState extends State { + + 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) { + 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".needTranslation, + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + SizedBox(height: 16.h), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + 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, + ), + ], + ), + ], + ), + ), + ), + + 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: _openDirection, + child: Text( + "Get Direction".needTranslation, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + 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: _resetDirection, + // child: Text( + // "Reset Direction".needTranslation, + // 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 c57ffb9..059969c 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -31,6 +31,17 @@ import 'package:hmg_patient_app_new/presentation/water_monitor/water_consumption import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_page.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'; + class AppRoutes { static const String initialRoute = '/initialRoute'; static const String loginScreen = '/loginScreen'; @@ -66,6 +77,8 @@ class AppRoutes { static const String triagePage = '/triageProgressScreen'; static const String userInfoSelection = '/userInfoSelection'; static const String userInfoFlowManager = '/userInfoFlowManager'; + static const String monthlyReports = '/monthlyReportsPage'; + static const String qrParking = '/qrParkingPage'; // Health Trackers static const String healthTrackersPage = '/healthTrackersListScreen'; @@ -113,6 +126,17 @@ class AppRoutes { return HealthTrackerDetailPage( trackerType: args ?? HealthTrackerTypeEnum.bloodSugar, ); + + monthlyReports: (context) => ChangeNotifierProvider( + create: (_) => MonthlyReportsViewModel( + monthlyReportsRepo: getIt(), + errorHandlerService: getIt(), + ), + child: const MonthlyReportsPage(), + ), + qrParking: (context) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), }, }; } diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 3c009f3..3092674 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}); @@ -33,6 +35,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 } @@ -160,6 +164,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 { diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 043eefd..b536661 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -26,6 +26,7 @@ import 'package:lottie/lottie.dart'; import 'core/cache_consts.dart'; import 'core/utils/push_notification_handler.dart'; + class SplashPage extends StatefulWidget { const SplashPage({super.key}); diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index aee425c..9fbdc17 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -110,4 +110,6 @@ class AppColors { static const Color shimmerBaseColor = Color(0xFFE0E0E0); // #E0E0E0 static const Color shimmerHighlightColor = Color(0xFFF5F5F5); // #F5F5F5 static const Color covid29Color = Color(0xff2563EB); // #2563EB + static const Color lightGreyTextColor = Color(0xFF959595); + static const Color labelColorYellow = Color(0xFFFBCB6E); } 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(', ')}')), +// ); +// } +// } +// } +// +// +//