Merge pull request 'nearest er module added' (#84) from nearest_er into master
Reviewed-on: #84ambulance_ui
						commit
						916ebf2880
					
				| @ -0,0 +1,3 @@ | ||||
| <svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||
|     <path fill-rule="evenodd" clip-rule="evenodd" d="M6.99996 1.22925C3.53667 1.22925 0.729126 4.0368 0.729126 7.50008C0.729126 10.9634 3.53667 13.7709 6.99996 13.7709C10.4632 13.7709 13.2708 10.9634 13.2708 7.50008C13.2708 4.0368 10.4632 1.22925 6.99996 1.22925ZM9.74577 5.57923C9.97358 5.35142 9.97358 4.98207 9.74577 4.75427C9.51797 4.52646 9.14862 4.52646 8.92081 4.75427L7 6.67509L5.95409 5.62925C5.72628 5.40146 5.35693 5.40147 5.12913 5.62928C4.90133 5.8571 4.90135 6.22644 5.12916 6.45424L6.17504 7.50004L6.00415 7.67094C5.77634 7.89874 5.77634 8.26809 6.00415 8.49589C6.23195 8.7237 6.6013 8.7237 6.8291 8.49589L7.00002 8.32497L7.17076 8.49569C7.39857 8.72349 7.76792 8.72348 7.99571 8.49567C8.22351 8.26785 8.2235 7.89851 7.99569 7.67071L7.82498 7.50002L9.74577 5.57923Z" fill="#2E3039"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 902 B | 
| @ -0,0 +1,19 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:ui'; | ||||
| 
 | ||||
| class Debouncer { | ||||
|   final int milliseconds; | ||||
|   VoidCallback? action; | ||||
|   Timer? _timer; | ||||
| 
 | ||||
|   Debouncer({required this.milliseconds}); | ||||
| 
 | ||||
|   void run(VoidCallback action) { | ||||
|     _timer?.cancel(); | ||||
|     _timer = Timer(Duration(milliseconds: milliseconds), action); | ||||
|   } | ||||
| 
 | ||||
|   void dispose() { | ||||
|     _timer?.cancel(); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,56 @@ | ||||
| import 'package:dartz/dartz.dart'; | ||||
| import 'package:hmg_patient_app_new/core/api/api_client.dart'; | ||||
| import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; | ||||
| import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; | ||||
| import 'package:hmg_patient_app_new/features/emergency_services/model/resp_model/ProjectAvgERWaitingTime.dart'; | ||||
| import 'package:hmg_patient_app_new/services/logger_service.dart'; | ||||
| 
 | ||||
| import '../../core/api_consts.dart' show GET_NEAREST_HOSPITAL; | ||||
| 
 | ||||
| abstract interface class EmergencyServicesRepo { | ||||
|    Future<Either<Failure, GenericApiModel<List<ProjectAvgERWaitingTime>>>> getNearestEr({int? id, int? projectID}); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class EmergencyServicesRepoImpl implements EmergencyServicesRepo { | ||||
|   final ApiClient apiClient; | ||||
|   final LoggerService loggerService; | ||||
| 
 | ||||
|   EmergencyServicesRepoImpl({required this.loggerService, required this.apiClient}); | ||||
|   @override | ||||
|   Future<Either<Failure, GenericApiModel<List<ProjectAvgERWaitingTime>>>> getNearestEr({int? id, int? projectID}) async { | ||||
|     Map<String, dynamic> mapDevice = {'IsForER' : true}; | ||||
| 
 | ||||
|     try { | ||||
|       GenericApiModel<List<ProjectAvgERWaitingTime>>? apiResponse; | ||||
|       Failure? failure; | ||||
|       await apiClient.post( | ||||
|         GET_NEAREST_HOSPITAL, | ||||
|         body: mapDevice, | ||||
|         onFailure: (error, statusCode, {messageStatus, failureType}) { | ||||
|           failure = failureType; | ||||
|         }, | ||||
|         onSuccess: (response, statusCode, {messageStatus, errorMessage}) { | ||||
|           try { | ||||
|             final list = response['List_ProjectAvgERWaitingTime']; | ||||
| 
 | ||||
|             final clinicsList = list.map((item) => ProjectAvgERWaitingTime.fromJson(item as Map<String, dynamic>)).toList().cast<ProjectAvgERWaitingTime>(); | ||||
|             apiResponse = GenericApiModel<List<ProjectAvgERWaitingTime>>( | ||||
|               messageStatus: messageStatus, | ||||
|               statusCode: statusCode, | ||||
|               errorMessage: null, | ||||
|               data: clinicsList, | ||||
|             ); | ||||
|           } 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())); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,103 @@ | ||||
| import 'dart:async' show FutureOr; | ||||
| 
 | ||||
| import 'package:flutter/foundation.dart' show ChangeNotifier; | ||||
| import 'package:hmg_patient_app_new/core/app_state.dart'; | ||||
| import 'package:hmg_patient_app_new/core/location_util.dart'; | ||||
| import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_repo.dart'; | ||||
| import 'package:hmg_patient_app_new/features/emergency_services/model/resp_model/ProjectAvgERWaitingTime.dart'; | ||||
| import 'package:hmg_patient_app_new/presentation/emergency_services/nearest_er_page.dart'; | ||||
| import 'package:hmg_patient_app_new/services/navigation_service.dart'; | ||||
| import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart' show canLaunchUrl, launchUrl, LaunchMode; | ||||
| 
 | ||||
| class EmergencyServicesViewModel extends ChangeNotifier { | ||||
|   final NavigationService navServices; | ||||
|   final LocationUtils? locationUtils; | ||||
|   final EmergencyServicesRepo viewModel; | ||||
|   final AppState appState; | ||||
|   bool isERListLoading = false; | ||||
|   List<ProjectAvgERWaitingTime> nearestERList = []; | ||||
|   List<ProjectAvgERWaitingTime> nearestERFilteredList = []; | ||||
| 
 | ||||
|   EmergencyServicesViewModel( | ||||
|       {required this.navServices, | ||||
|       required this.locationUtils, | ||||
|       required this.viewModel, | ||||
|       required this.appState}); | ||||
| 
 | ||||
|   void navigateToNearestERPage() { | ||||
|     locationUtils!.getLocation( | ||||
|         isShowConfirmDialog: true, | ||||
|         onSuccess: (position) { | ||||
|           navServices.push( | ||||
|             CustomPageRoute( | ||||
|               page: NearestErPage(), | ||||
|             ), | ||||
|           ); | ||||
|           getNearestER(); | ||||
|         }); | ||||
|   } | ||||
| 
 | ||||
|   void filterErList(String query) { | ||||
|     print ("the query is $query"); | ||||
|     if (query.isEmpty) { | ||||
|       nearestERFilteredList = nearestERList; | ||||
|     } else { | ||||
|       nearestERFilteredList = nearestERList | ||||
|           .where((er) => | ||||
|               er.projectName != null && | ||||
|               er.projectName!.toLowerCase().contains(query.toLowerCase())) | ||||
|           .toList(); | ||||
|     } | ||||
|     notifyListeners(); | ||||
|   } | ||||
| 
 | ||||
|   // Open directions (navigation) from current location to destination. | ||||
|   Future<void> openDirections({ | ||||
|     required double destLat, | ||||
|     required double destLng, | ||||
|     String? travelMode, // driving, walking, bicycling, transit | ||||
|   }) async { | ||||
|     // Try Google Maps app navigation intent (android/iOS) | ||||
|     final modeParam = travelMode == null ? 'driving' : travelMode; | ||||
|     final googleNavUri = Uri.parse('google.navigation:q=$destLat,$destLng&mode=${modeParam.substring(0,1)}'); // mode: d/w/b/t by scheme | ||||
|     final universalUrl = Uri.parse('https://www.google.com/maps/dir/?api=1&destination=$destLat,$destLng&travelmode=$modeParam'); | ||||
| 
 | ||||
|     if (await canLaunchUrl(googleNavUri)) { | ||||
|       await launchUrl(googleNavUri, mode: LaunchMode.externalApplication); | ||||
|     } else { | ||||
|       await launchUrl(universalUrl, mode: LaunchMode.externalApplication); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<void> openDialer(String phoneNumber) async { | ||||
|     final Uri telUri = Uri(scheme: 'tel', path: phoneNumber); | ||||
| 
 | ||||
|     if (await canLaunchUrl(telUri)) { | ||||
|       await launchUrl(telUri, mode: LaunchMode.externalApplication); | ||||
|     } else { | ||||
|       throw 'Could not open dialer for $phoneNumber'; | ||||
|     } | ||||
|   } | ||||
|   FutureOr<void> getNearestER() async { | ||||
|     isERListLoading = true; | ||||
|     nearestERList = []; | ||||
|     nearestERFilteredList = []; | ||||
|     notifyListeners(); | ||||
|     var response = await viewModel.getNearestEr(); | ||||
|     isERListLoading = false; | ||||
|     notifyListeners(); | ||||
| 
 | ||||
|     response.fold( | ||||
|       (failure) async { | ||||
|       }, | ||||
|       (apiResponse) { | ||||
|         isERListLoading = false; | ||||
|         if (apiResponse.messageStatus == 1) { | ||||
|           nearestERList = apiResponse.data!; | ||||
|           nearestERFilteredList = nearestERList; | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,114 @@ | ||||
| class ProjectAvgERWaitingTime { | ||||
|   int? iD; | ||||
|   int? projectID; | ||||
|   int? avgTimeInMinutes; | ||||
|   String? avgTimeInHHMM; | ||||
|   dynamic distanceInKilometers; | ||||
|   String? latitude; | ||||
|   String? longitude; | ||||
|   String? phonenumber; | ||||
|   String? projectImageURL; | ||||
|   String? projectName; | ||||
| 
 | ||||
|   ProjectAvgERWaitingTime( | ||||
|       {this.iD, | ||||
|         this.projectID, | ||||
|         this.avgTimeInMinutes, | ||||
|         this.avgTimeInHHMM, | ||||
|         this.distanceInKilometers, | ||||
|         this.latitude, | ||||
|         this.longitude, | ||||
|         this.phonenumber, | ||||
|         this.projectImageURL, | ||||
|         this.projectName}); | ||||
| 
 | ||||
|   ProjectAvgERWaitingTime.fromJson(Map<String, dynamic> json) { | ||||
|     iD = json['ID']; | ||||
|     projectID = json['ProjectID']; | ||||
|     avgTimeInMinutes = json['AvgTimeInMinutes']; | ||||
|     avgTimeInHHMM = json['AvgTimeInHHMM']; | ||||
|     distanceInKilometers = json['DistanceInKilometers']; | ||||
|     latitude = json['Latitude']; | ||||
|     longitude = json['Longitude']; | ||||
|     phonenumber = json['PhoneNumber']; | ||||
|     projectImageURL = json['ProjectImageURL']; | ||||
|     projectName = json['ProjectName']; | ||||
|   } | ||||
| 
 | ||||
|   String getTime(){ | ||||
|     print("the name is $projectName"); | ||||
|     print("the avgTimeInMinutes is $avgTimeInMinutes"); | ||||
|     if(avgTimeInMinutes == null) return ""; | ||||
|     int hours = avgTimeInMinutes! ~/ 60; | ||||
|     int minutes = avgTimeInMinutes! % 60; | ||||
|     print("the time is ${"${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}"}"); | ||||
|     return "${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}"; | ||||
|   } | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final Map<String, dynamic> data = new Map<String, dynamic>(); | ||||
|     data['ID'] = this.iD; | ||||
|     data['ProjectID'] = this.projectID; | ||||
|     data['AvgTimeInMinutes'] = this.avgTimeInMinutes; | ||||
|     data['AvgTimeInHHMM'] = this.avgTimeInHHMM; | ||||
|     data['DistanceInKilometers'] = this.distanceInKilometers; | ||||
|     data['Latitude'] = this.latitude; | ||||
|     data['Longitude'] = this.longitude; | ||||
|     data['PhoneNumber'] = this.phonenumber; | ||||
|     data['ProjectImageURL'] = this.projectImageURL; | ||||
|     data['ProjectName'] = this.projectName; | ||||
|     return data; | ||||
|   } | ||||
| } | ||||
| //class ProjectAvgERWaitingTime { | ||||
| //  int? iD; | ||||
| //  int? projectID; | ||||
| //  int? avgTimeInMinutes; | ||||
| //  String? avgTimeInHHMM; | ||||
| //  String? distanceInKilometers; | ||||
| //  String? latitude; | ||||
| //  String? longitude; | ||||
| //  String? phonenum?ber; | ||||
| //  String? projectImageURL; | ||||
| //  String? projectName; | ||||
| // | ||||
| //  ProjectAvgERWaitingTime( | ||||
| //      {this.iD, | ||||
| //        this.projectID, | ||||
| //        this.avgTimeInMinutes, | ||||
| //        this.avgTimeInHHMM, | ||||
| //        this.distanceInKilometers, | ||||
| //        this.latitude, | ||||
| //        this.longitude, | ||||
| //        this.phonenum?ber, | ||||
| //        this.projectImageURL, | ||||
| //        this.projectName}); | ||||
| // | ||||
| //  ProjectAvgERWaitingTime.fromJson(Map<String, dynamic> json) { | ||||
| //    iD = json['ID']; | ||||
| //    projectID = json['ProjectID']; | ||||
| //    avgTimeInMinutes = json['AvgTimeInMinutes']; | ||||
| //    avgTimeInHHMM = json['AvgTimeInHHMM']; | ||||
| //    distanceInKilometers = json['DistanceInKilometers']; | ||||
| //    latitude = json['Latitude']; | ||||
| //    longitude = json['Longitude']; | ||||
| //    phonenum?ber = json['Phonenum?ber']; | ||||
| //    projectImageURL = json['ProjectImageURL']; | ||||
| //    projectName = json['ProjectName']; | ||||
| //  } | ||||
| // | ||||
| //  Map<String, dynamic> toJson() { | ||||
| //    final Map<String, dynamic> data = new Map<String, dynamic>(); | ||||
| //    data['ID'] = this.iD; | ||||
| //    data['ProjectID'] = this.projectID; | ||||
| //    data['AvgTimeInMinutes'] = this.avgTimeInMinutes; | ||||
| //    data['AvgTimeInHHMM'] = this.avgTimeInHHMM; | ||||
| //    data['DistanceInKilometers'] = this.distanceInKilometers; | ||||
| //    data['Latitude'] = this.latitude; | ||||
| //    data['Longitude'] = this.longitude; | ||||
| //    data['Phonenum?ber'] = this.phonenum?ber; | ||||
| //    data['ProjectImageURL'] = this.projectImageURL; | ||||
| //    data['ProjectName'] = this.projectName; | ||||
| //    return data; | ||||
| //  } | ||||
| //} | ||||
| @ -0,0 +1,130 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hmg_patient_app_new/core/app_assets.dart'; | ||||
| import 'package:hmg_patient_app_new/core/app_export.dart'; | ||||
| import 'package:hmg_patient_app_new/core/utils/utils.dart'; | ||||
| import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; | ||||
| import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; | ||||
| import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; | ||||
| import 'package:hmg_patient_app_new/features/emergency_services/model/resp_model/ProjectAvgERWaitingTime.dart'; | ||||
| import 'package:hmg_patient_app_new/theme/colors.dart'; | ||||
| import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; | ||||
| import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| 
 | ||||
| class NearestERItem extends StatelessWidget { | ||||
|   final ProjectAvgERWaitingTime nearestERItem; | ||||
|   final bool isLoading; | ||||
| 
 | ||||
| 
 | ||||
|   const NearestERItem({ super.key, | ||||
|     required this.nearestERItem, | ||||
|     required this.isLoading | ||||
|   }) ; | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Container( | ||||
|       decoration: RoundedRectangleBorder().toSmoothCornerDecoration( | ||||
|         color: Colors.white, | ||||
|         customBorder: BorderRadius.only( | ||||
|           topLeft: Radius.circular(24.h), | ||||
|           topRight: Radius.circular(24.h), | ||||
|         ), | ||||
|       ), | ||||
|       child: Padding( | ||||
|         padding: EdgeInsets.all(16.h), | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Row( | ||||
|               children: [ | ||||
|                 (isLoading || nearestERItem.projectImageURL?.isEmpty == true) | ||||
|                     ? Container( | ||||
|                   width: 24.h, | ||||
|                   height: 24.h, | ||||
|                   decoration: BoxDecoration( | ||||
|                     color: Colors.grey.shade300, | ||||
|                     shape: BoxShape.circle, | ||||
|                   ), | ||||
|                 ).toShimmer2(isShow: isLoading) | ||||
|                     : Utils.buildImgWithNetwork( | ||||
|                   url: nearestERItem.projectImageURL ?? '', | ||||
|                   iconColor: Colors.transparent, | ||||
|                 ).circle(24.h).toShimmer2(isShow: isLoading), | ||||
|                 const SizedBox(width: 12), | ||||
|                 Expanded( | ||||
|                   child: (nearestERItem.projectName?.toText16( | ||||
|                     color: AppColors.textColor, | ||||
|                     weight: FontWeight.w600, | ||||
|                   ) ?? | ||||
|                       SizedBox.shrink()).toShimmer2(isShow: isLoading), | ||||
|                 ), | ||||
|                 // TODO: Add hospital icon logic here if needed | ||||
|               ], | ||||
|             ), | ||||
|             SizedBox(height: 8.h), | ||||
|             Row( | ||||
|               spacing: 8.h, | ||||
|               children: [ | ||||
|                 AppCustomChipWidget( | ||||
|                   labelText: "${nearestERItem.distanceInKilometers} km".needTranslation, | ||||
|                   icon: AppAssets.location, | ||||
|                   iconHasColor: false, | ||||
|                   labelPadding: EdgeInsetsDirectional.only(start: 4.h, end: 0.h), | ||||
|                   padding: EdgeInsets.all(8.h), | ||||
|                 ).toShimmer2(isShow: isLoading), | ||||
|                 AppCustomChipWidget( | ||||
|                   labelText: "Expected waiting time: ${nearestERItem.getTime()} mins".needTranslation, | ||||
|                   icon: AppAssets.waiting_time_clock, | ||||
|                   iconHasColor: false, | ||||
|                   labelPadding: EdgeInsetsDirectional.only(start: 4.h, end: 0.h), | ||||
|                   padding: EdgeInsets.all(8.h), | ||||
|                 ).toShimmer2(isShow: isLoading), | ||||
|               ], | ||||
|             ), | ||||
|             SizedBox(height: 16.h), | ||||
|             Row( | ||||
|               children: [ | ||||
|                 Expanded( | ||||
|                   child: CustomButton( | ||||
|                     text: "View Location on Google Maps".needTranslation, | ||||
|                     iconSize: 18.h, | ||||
|                     icon: AppAssets.location, | ||||
|                     onPressed: () { | ||||
|                       context.read<EmergencyServicesViewModel>().openDirections(destLat:  double.parse(nearestERItem.latitude??"0.0"), destLng: double.parse(nearestERItem.longitude??"0.0") ); | ||||
|                     }, | ||||
|                     backgroundColor: AppColors.secondaryLightRedColor, | ||||
|                     borderColor: AppColors.secondaryLightRedColor, | ||||
|                     textColor: AppColors.primaryRedColor, | ||||
|                     iconColor: AppColors.primaryRedColor, | ||||
|                     height: 40.h, | ||||
|                     fontSize: 14, | ||||
|                     fontWeight: FontWeight.w500, | ||||
|                   ).toShimmer2(isShow: isLoading), | ||||
|                 ), | ||||
|                 SizedBox(width: 8.h), | ||||
|                 SizedBox( | ||||
|                   height: 40.h, | ||||
|                   width: 40.h, | ||||
|                   child: CustomButton( | ||||
|                     text: '', | ||||
|                     iconSize: 18.h, | ||||
|                     icon: AppAssets.call_fill, | ||||
|                     onPressed: () { | ||||
|                       context.read<EmergencyServicesViewModel>().openDialer( nearestERItem.phonenumber??""); | ||||
| 
 | ||||
|                     }, | ||||
|                     backgroundColor: AppColors.greyColor, | ||||
|                     iconColor: AppColors.textColor, | ||||
|                     borderColor: AppColors.greyColor, | ||||
|                     height: 40.h, | ||||
|                   ).toShimmer2(isShow: isLoading), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
					Loading…
					
					
				
		Reference in New Issue