Merge pull request 'haroon_dev' (#181) from haroon_dev into master
Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/181master
commit
48718a3406
@ -0,0 +1,6 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgaPetsOq3tqUlswBN
|
||||
BFUWIIdEg4dcTCGRUKbAEx8ViFygCgYIKoZIzj0DAQehRANCAAS6IKkZ4tCeBlN6
|
||||
DtXiBekCiIbgHxWk8AWppP0H3mJ+I2s8cIEaquL4/yeLDcHzAHf8tHcYhF/TvggT
|
||||
nc30yDLv
|
||||
-----END PRIVATE KEY-----
|
||||
Binary file not shown.
Binary file not shown.
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue