Services price list implemented

pull/181/head
haroon amjad 1 month ago
parent 2ab3e178a2
commit 3c9c3032a5

@ -1528,5 +1528,9 @@
"confirmYourLocation": "قم بتأكيد موقعك",
"confirmYourLocationDesc": "يرجى تأكيد موقعك الحالي لعرض الاتجاهات المناسبة.",
"insideHospital": "أنا داخل المستشفى",
"outsideHospital": "أنا خارج المستشفى"
"outsideHospital": "أنا خارج المستشفى",
"servicePriceList": "قائمة أسعار الخدمات",
"servicePriceListDesc": "توضح قائمة أسعار الخدمات أدناه رسوم الخدمات الصحية المقدمة للمرضى بنظام الدفع النقدي. أما فيما يتعلق بالخدمات المشمولة بالتأمين، فسيتم تطبيق التغطية التأمينية والتحقق من الأهلية واحتساب نسب التحمل وفقًا لشروط وثيقة التأمين وجدول المنافع المعتمد لكل شركة تأمين.",
"servicePriceListRights": "يحق للمريض الحصول على متابعة مجانية في غضون 14 يومًا من الزيارة الأولى",
"serviceName": "اسم الخدمة"
}

@ -1520,5 +1520,10 @@
"confirmYourLocation": "Confirm Your Location",
"confirmYourLocationDesc": "Please confirm your present location to view appropriate directions.",
"insideHospital": "I am inside the hospital",
"outsideHospital": "I am outside the hospital"
"outsideHospital": "I am outside the hospital",
"servicePriceList": "Services Price List",
"servicePriceListDesc": "Below is the services price list outline the healthcare services fees for cash payments, where the insurance coverage, eligibility, and co-payment deductions will be processed in accordance with the insurance policy terms and the table of benefits of each insurance providers:",
"servicePriceListRights": "The patient has the right to a free follow-up within 14 days of initial visit",
"serviceName": "Service Name"
}

@ -380,6 +380,8 @@ var GET_PATIENT_SICK_LEAVE = 'Services/Patients.svc/REST/GetPatientSickLeave';
var GET_PATIENT_SICK_LEAVE_STATUS = 'Services/Patients.svc/REST/GetPatientSickLeave_Status';
var GET_SERVICES_PRICE_LIST = 'Services/OUTPs.svc/REST/GetServicesPriceList';
var SendSickLeaveEmail = 'Services/Notifications.svc/REST/SendSickLeaveEmail';
var GET_PATIENT_AdVANCE_BALANCE_AMOUNT = 'Services/Patients.svc/REST/GetPatientAdvanceBalanceAmount';
@ -681,7 +683,7 @@ const DASHBOARD = 'Services/Patients.svc/REST/PatientDashboard';
class ApiConsts {
static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT

@ -54,6 +54,8 @@ import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_v
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/services_price_list/services_price_list_repo.dart';
import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_view_model.dart';
import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
@ -169,6 +171,7 @@ class AppDependencies {
getIt.registerLazySingleton<QrParkingRepo>(() => QrParkingRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<NotificationsRepo>(() => NotificationsRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<AskDoctorRepo>(() => AskDoctorRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
getIt.registerLazySingleton<ServicesPriceListRepo>(() => ServicesPriceListRepoImp(loggerService: getIt<LoggerService>(), apiClient: getIt()));
// ViewModels
// Global/shared VMs LazySingleton
@ -311,5 +314,6 @@ class AppDependencies {
getIt.registerLazySingleton<AskDoctorViewModel>(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<ServicesPriceListViewModel>(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt()));
}
}

@ -0,0 +1,54 @@
class ServicesPriceListResponseModel {
int? createdBy;
String? createdOn;
int? editedBy;
String? editedOn;
int? id;
bool? isEnabled;
String? nameAR;
String? nameEN;
num? price;
int? rowID;
ServicesPriceListResponseModel({
this.createdBy,
this.createdOn,
this.editedBy,
this.editedOn,
this.id,
this.isEnabled,
this.nameAR,
this.nameEN,
this.price,
this.rowID,
});
ServicesPriceListResponseModel.fromJson(Map<String, dynamic> json) {
createdBy = json['CreatedBy'];
createdOn = json['CreatedOn'];
editedBy = json['EditedBy'];
editedOn = json['EditedOn'];
id = json['ID'];
isEnabled = json['IsEnabled'];
nameAR = json['NameAR'];
nameEN = json['NameEN'];
price = json['Price'];
rowID = json['RowID'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['CreatedBy'] = createdBy;
data['CreatedOn'] = createdOn;
data['EditedBy'] = editedBy;
data['EditedOn'] = editedOn;
data['ID'] = id;
data['IsEnabled'] = isEnabled;
data['NameAR'] = nameAR;
data['NameEN'] = nameEN;
data['Price'] = price;
data['RowID'] = rowID;
return data;
}
}

@ -0,0 +1,80 @@
import 'package:dartz/dartz.dart';
import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.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/services_price_list/models/resp_models/services_price_list_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class ServicesPriceListRepo {
Future<Either<Failure, GenericApiModel<List<ServicesPriceListResponseModel>>>> getServicesPriceList({
String searchKey = "",
int pageIndex = 0,
int pageSize = 0,
});
}
class ServicesPriceListRepoImp implements ServicesPriceListRepo {
final ApiClient apiClient;
final LoggerService loggerService;
ServicesPriceListRepoImp({required this.loggerService, required this.apiClient});
@override
Future<Either<Failure, GenericApiModel<List<ServicesPriceListResponseModel>>>> getServicesPriceList({
String searchKey = "",
int pageIndex = 0,
int pageSize = 0,
}) async {
Map<String, dynamic> mapDevice = {
"ID": 1,
"SearchKey": searchKey,
"PageIndex": pageIndex,
"PageSize": pageSize,
"RowCount": 0,
"TokenID": "@dm!n"
};
try {
GenericApiModel<List<ServicesPriceListResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_SERVICES_PRICE_LIST,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final list = response['getServicesPriceList'];
if (list == null || list.isEmpty) {
throw Exception("Services price list is empty");
}
final servicesList = list
.map((item) => ServicesPriceListResponseModel.fromJson(item as Map<String, dynamic>))
.toList()
.cast<ServicesPriceListResponseModel>();
apiResponse = GenericApiModel<List<ServicesPriceListResponseModel>>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: servicesList,
);
} 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,87 @@
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/features/services_price_list/models/resp_models/services_price_list_response_model.dart';
import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
class ServicesPriceListViewModel extends ChangeNotifier {
bool isServicesPriceListLoading = false;
ServicesPriceListRepo servicesPriceListRepo;
ErrorHandlerService errorHandlerService;
List<ServicesPriceListResponseModel> servicesPriceList = [];
List<ServicesPriceListResponseModel> filteredServicesPriceList = [];
String searchKey = "";
ServicesPriceListViewModel({
required this.servicesPriceListRepo,
required this.errorHandlerService,
});
initServicesPriceListProvider() {
servicesPriceList.clear();
filteredServicesPriceList.clear();
isServicesPriceListLoading = true;
getServicesPriceList();
notifyListeners();
}
setIsServicesPriceListLoading(bool val) {
isServicesPriceListLoading = val;
notifyListeners();
}
setSearchKey(String val) {
searchKey = val;
filterServicesList(val);
}
void filterServicesList(String query) {
if (query.isEmpty) {
filteredServicesPriceList = List.from(servicesPriceList);
} else {
filteredServicesPriceList = servicesPriceList.where((service) {
final nameEN = service.nameEN?.toLowerCase() ?? '';
final nameAR = service.nameAR?.toLowerCase() ?? '';
final searchLower = query.toLowerCase();
return nameEN.contains(searchLower) || nameAR.contains(searchLower);
}).toList();
}
notifyListeners();
}
Future<void> getServicesPriceList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await servicesPriceListRepo.getServicesPriceList(
searchKey: searchKey,
);
result.fold(
(failure) async {
isServicesPriceListLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
isServicesPriceListLoading = false;
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Error loading services price list");
}
} else if (apiResponse.messageStatus == 1) {
servicesPriceList = apiResponse.data!;
servicesPriceList.removeWhere((element) => element.isEnabled == false);
filteredServicesPriceList = List.from(servicesPriceList);
isServicesPriceListLoading = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
}

@ -1519,5 +1519,9 @@ abstract class LocaleKeys {
static const confirmYourLocationDesc = 'confirmYourLocationDesc';
static const insideHospital = 'insideHospital';
static const outsideHospital = 'outsideHospital';
static const servicePriceList = 'servicePriceList';
static const servicePriceListDesc = 'servicePriceListDesc';
static const servicePriceListRights = 'servicePriceListRights';
static const serviceName = 'serviceName';
}

@ -36,6 +36,7 @@ import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_mo
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_view_model.dart';
import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_view_model.dart';
import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart';
import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart';
@ -202,6 +203,9 @@ void main() async {
),
ChangeNotifierProvider<AskDoctorViewModel>(
create: (_) => getIt.get<AskDoctorViewModel>(),
),
ChangeNotifierProvider<ServicesPriceListViewModel>(
create: (_) => getIt.get<ServicesPriceListViewModel>(),
)
], child: MyApp()),
), // Wrap your app

@ -29,6 +29,7 @@ import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dar
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/presentation/servicesPriceList/services_price_list_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';
@ -150,13 +151,27 @@ class ServicesPage extends StatelessWidget {
}),
HmgServicesComponentModel(
3,
"Blood Donation".needTranslation,
LocaleKeys.bloodDonation.tr(context: getIt.get<NavigationService>().navigatorKey.currentContext!),
"".needTranslation,
AppAssets.blood_donation_icon,
bgColor: AppColors.bloodDonationCardColor,
true,
route: AppRoutes.bloodDonationPage,
),
HmgServicesComponentModel(
113,
LocaleKeys.servicePriceList.tr(context: getIt.get<NavigationService>().navigatorKey.currentContext!),
"".needTranslation,
AppAssets.saudi_riyal_icon,
bgColor: AppColors.textColor,
false,
route: null,
onTap: () {
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(page: ServicesPriceListPage()),
);
},
)
// HmgServicesComponentModel(
// 3,
// "My Child Vaccine".needTranslation,
@ -302,6 +317,9 @@ class ServicesPage extends StatelessWidget {
Widget build(BuildContext context) {
bloodDonationViewModel = Provider.of<BloodDonationViewModel>(context);
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context);
hmgServices.removeWhere((element) => Utils.havePrivilege(element.action) == false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
@ -382,7 +400,7 @@ class ServicesPage extends StatelessWidget {
children: [
Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h),
LocaleKeys.habibWallet.tr().toText14(weight: FontWeight.w600, maxlines: 2).expanded,
Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward),
Utils.buildSvgWithAssets(icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward),
],
),
Spacer(),
@ -442,7 +460,7 @@ class ServicesPage extends StatelessWidget {
children: [
Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h),
LocaleKeys.medicalFile.tr().toText16(weight: FontWeight.w600, maxlines: 2).expanded,
Utils.buildSvgWithAssets(icon: AppAssets.arrow_forward),
Utils.buildSvgWithAssets(icon: getIt.get<AppState>().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward),
],
),
Spacer(),
@ -667,7 +685,7 @@ class ServicesPage extends StatelessWidget {
fit: BoxFit.contain,
),
SizedBox(width: 8.w),
LocaleKeys.hmgContact.tr().toText14(weight: FontWeight.w500)
Expanded(child: LocaleKeys.hmgContact.tr().toText14(weight: FontWeight.w500))
],
),
),

@ -0,0 +1,199 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class ServicesPriceListPage extends StatefulWidget {
const ServicesPriceListPage({super.key});
@override
State<ServicesPriceListPage> createState() => _ServicesPriceListPageState();
}
class _ServicesPriceListPageState extends State<ServicesPriceListPage> {
late ServicesPriceListViewModel servicesPriceListViewModel;
late AppState appState;
@override
void initState() {
scheduleMicrotask(() {
servicesPriceListViewModel.initServicesPriceListProvider();
});
super.initState();
}
@override
Widget build(BuildContext context) {
servicesPriceListViewModel = Provider.of<ServicesPriceListViewModel>(context, listen: false);
appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: LocaleKeys.servicePriceList.tr(context: context),
child: SingleChildScrollView(
padding: EdgeInsets.all(24.h),
child: Consumer<ServicesPriceListViewModel>(builder: (context, model, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: LocaleKeys.servicePriceListDesc.tr(context: context).toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
),
),
SizedBox(height: 16.h),
model.isServicesPriceListLoading
? ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
itemCount: 5,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) => _buildLoadingCard(),
)
: model.filteredServicesPriceList.isEmpty
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
padding: EdgeInsets.all(40.h),
child: Center(
child: LocaleKeys.noDataAvailable.tr(context: context).toText16(
color: AppColors.textColorLight,
),
),
)
: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.serviceName.tr(context: context).toText18(weight: FontWeight.bold, color: AppColors.textColor),
SizedBox(height: 16.h),
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
itemCount: model.filteredServicesPriceList.length,
separatorBuilder: (context, index) => Divider(height: 1.h, color: AppColors.dividerColor).withVerticalPadding(),
itemBuilder: (context, index) {
final service = model.filteredServicesPriceList[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: (appState.isArabic() ? service.nameAR ?? service.nameEN ?? '' : service.nameEN ?? service.nameAR ?? '').toText16(
weight: FontWeight.w500,
color: AppColors.textColor,
),
),
SizedBox(width: 12.w),
Utils.getPaymentAmountWithSymbol('${service.price ?? 0}'.toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true),
],
),
],
),
),
),
);
},
),
],
),
),
SizedBox(height: 16.h),
Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: LocaleKeys.servicePriceListRights.tr(context: context).toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight),
),
),
],
);
}),
),
),
);
}
Widget _buildLoadingCard() {
return Shimmer.fromColors(
baseColor: AppColors.greyColor.withValues(alpha: 0.3),
highlightColor: AppColors.greyColor.withValues(alpha: 0.1),
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Container(
height: 20.h,
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(4.r),
),
),
),
SizedBox(width: 12.w),
Container(
width: 80.w,
height: 32.h,
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(12.r),
),
),
],
),
),
),
);
}
}

@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/presentation/health_trackers/health_trackers
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart';
import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart';
import 'package:hmg_patient_app_new/presentation/servicesPriceList/services_price_list_page.dart';
import 'package:hmg_patient_app_new/presentation/smartwatches/huawei_health_example.dart';
import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_home_page.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart';
@ -88,6 +89,9 @@ class AppRoutes {
// Emergency Services
static const String emergencyServicesPage = '/emergencyServicesPage';
// Services Price List
static const String servicesPriceListPage = '/servicesPriceListPage';
static Map<String, WidgetBuilder> get routes => {
initialRoute: (context) => SplashPage(),
loginScreen: (context) => LoginScreen(),
@ -120,6 +124,7 @@ class AppRoutes {
healthTrackersPage: (context) => HealthTrackersPage(),
vitalSign: (context) => VitalSignPage(),
emergencyServicesPage: (context) => EmergencyServicesPage(),
servicesPriceListPage: (context) => ServicesPriceListPage(),
addHealthTrackerEntryPage: (context) {
final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?;
return AddHealthTrackerEntryPage(

Loading…
Cancel
Save