diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 66b9b7e..b0fb2a8 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -207,7 +207,7 @@ var GET_APPOINTMENT_DETAILS_BY_NO = 'Services/MobileNotifications.svc/REST/GetAp var NEW_RATE_APPOINTMENT_URL = "Services/Doctors.svc/REST/AppointmentsRating_InsertAppointmentRate"; var NEW_RATE_DOCTOR_URL = "Services/Doctors.svc/REST/DoctorsRating_InsertDoctorRate"; -var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; +//var GET_QR_PARKING = 'Services/SWP.svc/REST/GetQRParkingByID'; //URL to get clinic list var GET_CLINICS_LIST_URL = "Services/lists.svc/REST/GetClinicCentralized"; @@ -700,7 +700,7 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -828,6 +828,7 @@ class ApiConsts { static final String getTermsConditions = 'Services/Patients.svc/Rest/GetUserTermsAndConditions'; static final String getMonthlyReports = 'Services/Patients.svc/Rest/UpdatePateintHealthSummaryReport'; static final String updatePatientEmail = 'Services/Patients.svc/Rest/UpdatePateintEmail'; + static final String getQrParkingDetails = 'Services/SWP.svc/REST/GetQRParkingByID'; // Ancillary Order Apis static final String getOnlineAncillaryOrderList = 'Services/Doctors.svc/REST/GetOnlineAncillaryOrderList'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 555ce29..872fe69 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -41,6 +41,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_repo.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; @@ -63,6 +64,7 @@ import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart'; import '../features/active_prescriptions/active_prescriptions_repo.dart'; @@ -140,6 +142,15 @@ class AppDependencies { getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt(), ),); getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => QrParkingRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + // ViewModels // Global/shared VMs → LazySingleton @@ -245,6 +256,14 @@ class AppDependencies { activePrescriptionsRepo: getIt() ), ); + getIt.registerFactory( + () => QrParkingViewModel( + qrParkingRepo: getIt(), + errorHandlerService: getIt(), + cacheService: getIt(), + ), + ); + // Screen-specific VMs → Factory // getIt.registerFactory( diff --git a/lib/features/qr_parking/models/qr_parking_response_model.dart b/lib/features/qr_parking/models/qr_parking_response_model.dart new file mode 100644 index 0000000..2e90da1 --- /dev/null +++ b/lib/features/qr_parking/models/qr_parking_response_model.dart @@ -0,0 +1,183 @@ + + +class QrParkingResponseModel { + dynamic totalRecords; + dynamic nRowID; + int? qRParkingID; + String? description; + String? descriptionN; + dynamic qRCompare; + dynamic qRValue; + String? imagePath; + bool? isActive; + int? parkingID; + int? branchID; + int? companyID; + int? buildingID; + int? rowID; + int? gateID; + int? floorID; + dynamic imagePath1; + int? createdBy; + String? createdOn; + dynamic editedBy; + dynamic editedOn; + String? parkingDescription; + String? parkingDescriptionN; + String? gateDescription; + String? gateDescriptionN; + String? branchDescription; + String? branchDescriptionN; + String? companyDescription; + String? companyDescriptionN; + String? rowDescription; + String? rowDescriptionN; + String? floorDescription; + String? floorDescriptionN; + String? buildingDescription; + String? buildingDescriptionN; + String? qRParkingCode; + String? parkingCode; + double? latitude; + double? longitude; + String? qRImageStr; + + QrParkingResponseModel({ + this.totalRecords, + this.nRowID, + this.qRParkingID, + this.description, + this.descriptionN, + this.qRCompare, + this.qRValue, + this.imagePath, + this.isActive, + this.parkingID, + this.branchID, + this.companyID, + this.buildingID, + this.rowID, + this.gateID, + this.floorID, + this.imagePath1, + this.createdBy, + this.createdOn, + this.editedBy, + this.editedOn, + this.parkingDescription, + this.parkingDescriptionN, + this.gateDescription, + this.gateDescriptionN, + this.branchDescription, + this.branchDescriptionN, + this.companyDescription, + this.companyDescriptionN, + this.rowDescription, + this.rowDescriptionN, + this.floorDescription, + this.floorDescriptionN, + this.buildingDescription, + this.buildingDescriptionN, + this.qRParkingCode, + this.parkingCode, + this.latitude, + this.longitude, + this.qRImageStr, + }); + + QrParkingResponseModel.fromJson(Map json) { + totalRecords = json['TotalRecords']; + nRowID = json['nRowID']; + qRParkingID = json['QRParkingID']; + description = json['Description']; + descriptionN = json['DescriptionN']; + qRCompare = json['QRCompare']; + qRValue = json['QRValue']; + imagePath = json['ImagePath']; + isActive = json['IsActive']; + parkingID = json['ParkingID']; + branchID = json['BranchID']; + companyID = json['CompanyID']; + buildingID = json['BuildingID']; + rowID = json['RowID']; + gateID = json['GateID']; + floorID = json['FloorID']; + imagePath1 = json['ImagePath1']; + createdBy = json['CreatedBy']; + createdOn = json['CreatedOn']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + parkingDescription = json['ParkingDescription']; + parkingDescriptionN = json['ParkingDescriptionN']; + gateDescription = json['GateDescription']; + gateDescriptionN = json['GateDescriptionN']; + branchDescription = json['BranchDescription']; + branchDescriptionN = json['BranchDescriptionN']; + companyDescription = json['CompanyDescription']; + companyDescriptionN = json['CompanyDescriptionN']; + rowDescription = json['RowDescription']; + rowDescriptionN = json['RowDescriptionN']; + floorDescription = json['FloorDescription']; + floorDescriptionN = json['FloorDescriptionN']; + buildingDescription = json['BuildingDescription']; + buildingDescriptionN = json['BuildingDescriptionN']; + qRParkingCode = json['QRParkingCode']; + parkingCode = json['ParkingCode']; + latitude = _toDouble(json['Latitude']); + longitude = _toDouble(json['Longitude']); + qRImageStr = json['QRImageStr']; + } + + Map toJson() { + final Map data = {}; + data['TotalRecords'] = totalRecords; + data['nRowID'] = nRowID; + data['QRParkingID'] = qRParkingID; + data['Description'] = description; + data['DescriptionN'] = descriptionN; + data['QRCompare'] = qRCompare; + data['QRValue'] = qRValue; + data['ImagePath'] = imagePath; + data['IsActive'] = isActive; + data['ParkingID'] = parkingID; + data['BranchID'] = branchID; + data['CompanyID'] = companyID; + data['BuildingID'] = buildingID; + data['RowID'] = rowID; + data['GateID'] = gateID; + data['FloorID'] = floorID; + data['ImagePath1'] = imagePath1; + data['CreatedBy'] = createdBy; + data['CreatedOn'] = createdOn; + data['EditedBy'] = editedBy; + data['EditedOn'] = editedOn; + data['ParkingDescription'] = parkingDescription; + data['ParkingDescriptionN'] = parkingDescriptionN; + data['GateDescription'] = gateDescription; + data['GateDescriptionN'] = gateDescriptionN; + data['BranchDescription'] = branchDescription; + data['BranchDescriptionN'] = branchDescriptionN; + data['CompanyDescription'] = companyDescription; + data['CompanyDescriptionN'] = companyDescriptionN; + data['RowDescription'] = rowDescription; + data['RowDescriptionN'] = rowDescriptionN; + data['FloorDescription'] = floorDescription; + data['FloorDescriptionN'] = floorDescriptionN; + data['BuildingDescription'] = buildingDescription; + data['BuildingDescriptionN'] = buildingDescriptionN; + data['QRParkingCode'] = qRParkingCode; + data['ParkingCode'] = parkingCode; + data['Latitude'] = latitude; + data['Longitude'] = longitude; + data['QRImageStr'] = qRImageStr; + return data; + } + + static double? _toDouble(dynamic v) { + if (v == null) return null; + if (v is double) return v; + if (v is int) return v.toDouble(); + return double.tryParse(v.toString()); + } +} + diff --git a/lib/features/qr_parking/qr_parking_repo.dart b/lib/features/qr_parking/qr_parking_repo.dart new file mode 100644 index 0000000..1ec905f --- /dev/null +++ b/lib/features/qr_parking/qr_parking_repo.dart @@ -0,0 +1,74 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; +import '../../core/api/api_client.dart'; +import '../../core/api_consts.dart'; +import '../../core/common_models/generic_api_model.dart'; +import '../../core/exceptions/api_failure.dart'; +import '../../services/logger_service.dart'; + + +abstract class QrParkingRepo { + Future>>> + getQrParking({ + required int qrParkingId, + }); +} + +class QrParkingRepoImp implements QrParkingRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + QrParkingRepoImp({ + required this.loggerService, + required this.apiClient, + }); + + @override + Future>>> + getQrParking({required int qrParkingId}) async { + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getQrParkingDetails, // GetQRParkingByID + body: {'QRParkingID': qrParkingId}, + onFailure: (error, statusCode, + {messageStatus, failureType}) { + failure = failureType ?? + StatusCodeFailure("$error ($statusCode)"); + }, + onSuccess: (response, statusCode, + {messageStatus, errorMessage}) { + final list = + (response['List_SWP_QRParkingModel'] as List?) ?? []; + + final res = list + .map((e) => QrParkingResponseModel.fromJson( + Map.from(e), + )) + .toList(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: res, + ); + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) { + return Left(ServerFailure("Unknown error")); + } + + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} + diff --git a/lib/features/qr_parking/qr_parking_view_model.dart b/lib/features/qr_parking/qr_parking_view_model.dart new file mode 100644 index 0000000..b0b688f --- /dev/null +++ b/lib/features/qr_parking/qr_parking_view_model.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; + +import '../../services/cache_service.dart'; +import '../../services/error_handler_service.dart'; +import 'models/qr_parking_response_model.dart'; + + +class QrParkingViewModel extends ChangeNotifier { + final QrParkingRepo qrParkingRepo; + final ErrorHandlerService errorHandlerService; + final CacheService cacheService; + String IS_GO_TO_PARKING = 'IS_GO_TO_PARKING'; + + bool isLoading = false; + String? error; + + bool isSavePark = false; + QrParkingResponseModel? qrParkingModel; + List qrParkingList = []; + + QrParkingViewModel({ + required this.qrParkingRepo, + required this.errorHandlerService, + required this.cacheService, + }); + + + Future scanAndGetParking() async { + try { + error = null; + isLoading = true; + notifyListeners(); + + final result = await BarcodeScanner.scan(); + + if (result.type != ResultType.Barcode) { + isLoading = false; + notifyListeners(); + return null; + } + + final raw = result.rawContent.trim(); + if (raw.isEmpty) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final qrParkingId = _extractQrParkingId(raw); + if (qrParkingId == null) { + error = "Invalid QR Code"; + isLoading = false; + notifyListeners(); + return null; + } + + final apiResult = + await qrParkingRepo.getQrParking(qrParkingId: qrParkingId); + + final model = apiResult.fold( + (failure) { + errorHandlerService.handleError(failure: failure); + error = failure.toString(); + return null; + }, + (apiResponse) { + qrParkingList = apiResponse.data ?? []; + if (qrParkingList.isNotEmpty) { + return qrParkingList.first; + } + error = "Invalid Qr Code"; + return null; + }, + ); + + if (model != null) { + qrParkingModel = model; + isSavePark = true; + + await cacheService.saveObject( + key: IS_GO_TO_PARKING, + value: model.toJson(), + ); + } + + isLoading = false; + notifyListeners(); + return model; + } catch (e) { + error = "Scan error"; + isLoading = false; + notifyListeners(); + return null; + } + } + + /// Load saved parking + Future getIsSaveParking() async { + isLoading = true; + notifyListeners(); + + final parking = + await cacheService.getObject(key: IS_GO_TO_PARKING); + + if (parking != null) { + isSavePark = true; + qrParkingModel = QrParkingResponseModel.fromJson( + Map.from(parking), + ); + } else { + isSavePark = false; + qrParkingModel = null; + } + + isLoading = false; + notifyListeners(); + } + + /// Reset parking + Future clearParking() async { + await cacheService.remove(key: IS_GO_TO_PARKING); + isSavePark = false; + qrParkingModel = null; + notifyListeners(); + } + + int? _extractQrParkingId(String raw) { + try { + if (raw.startsWith("{")) { + final data = jsonDecode(raw); + return int.tryParse(data['QRParkingID'].toString()); + } + return int.tryParse(raw); + } catch (_) { + return null; + } + } +} + + diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 87a0e0e..a3d5d76 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -21,7 +21,7 @@ import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; - +import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -31,10 +31,9 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; import '../../core/dependencies.dart' show getIt; +import '../../features/qr_parking/qr_parking_view_model.dart'; class ServicesPage extends StatelessWidget { ServicesPage({super.key}); @@ -89,30 +88,18 @@ class ServicesPage extends StatelessWidget { route: null, onTap: () async { LoaderBottomSheet.showLoader(loadingText: "Fetching Data..."); await bloodDonationViewModel.getRegionSelectedClinics(onSuccess: (val) async { - // await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { + await bloodDonationViewModel.getPatientBloodGroupDetails(onSuccess: (val) { LoaderBottomSheet.hideLoader(); Navigator.of(GetIt.instance().navigatorKey.currentContext!).push( CustomPageRoute( page: BloodDonationPage(), ), ); - // }, onError: (err) { - // LoaderBottomSheet.hideLoader(); - // }); + }); }, onError: (err) { LoaderBottomSheet.hideLoader(); }); }), - HmgServicesComponentModel( - 11, - "Covid 19 Test".needTranslation, - "".needTranslation, - AppAssets.covid19icon, - bgColor: AppColors.covid29Color, - true, - route: AppRoutes.covid19Test, - ) - // HmgServicesComponentModel( // 3, // "Home Health Care".needTranslation, @@ -176,30 +163,8 @@ class ServicesPage extends StatelessWidget { route: AppRoutes.smartWatches, // route: AppRoutes.huaweiHealthExample, ), - // HmgServicesComponentModel( - // 12, - // "Latest News".needTranslation, - // "".needTranslation, - // AppAssets.news, - // true, - // bgColor: AppColors.bgGreenColor, - // textColor: AppColors.blackColor, - // route: "https://twitter.com/HMG", - // isExternalLink: true, - // ), - HmgServicesComponentModel( - 12, - "Monthly Reports".needTranslation, - "".needTranslation, - AppAssets.report_icon, - true, - bgColor: AppColors.bgGreenColor, - textColor: AppColors.blackColor, - route: AppRoutes.monthlyReports, - ), ]; - @override Widget build(BuildContext context) { bloodDonationViewModel = Provider.of(context); @@ -467,43 +432,37 @@ class ServicesPage extends StatelessWidget { ), SizedBox(width: 16.w), Expanded( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ParkingPage(), - ), - ); - }, - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.car_parking_icon, - width: 32.w, - height: 32.h, - fit: BoxFit.contain, - ), - SizedBox(width: 8.w), - "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) - ], - ).onPress(() { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ParkingPage(), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.h, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.car_parking_icon, + width: 32.w, + height: 32.h, + fit: BoxFit.contain, + ), + SizedBox(width: 8.w), + "Car Parking".needTranslation.toText12(fontWeight: FontWeight.w500) + ], + ).onPress(() { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), ), - ); - }), - ), + ), + ); + + }), ), ), ), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 52dc9ff..80a45ac 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -1069,7 +1069,9 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.monthly_reports_icon, isLargeText: true, iconSize: 36.h, - ), + ).onPress(() { + Navigator.pushNamed(context, AppRoutes.monthlyReports); + }), MedicalFileCard( label: "Medical Reports".needTranslation, textColor: AppColors.blackColor, diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart index ce9b6ab..cd1e8bc 100644 --- a/lib/presentation/parking/paking_page.dart +++ b/lib/presentation/parking/paking_page.dart @@ -1,16 +1,18 @@ - import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/presentation/parking/parking_slot.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:provider/provider.dart'; +import '../../features/qr_parking/qr_parking_view_model.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/routes/custom_page_route.dart'; + class ParkingPage extends StatefulWidget { const ParkingPage({super.key}); @@ -19,10 +21,29 @@ class ParkingPage extends StatefulWidget { } class _ParkingPageState extends State { - String? scannedCode; + Future _readQR(BuildContext context) async { + final vm = context.read(); + + final model = await vm.scanAndGetParking(); + + if (model == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(vm.error ?? "Invalid Qr Code")), + ); + return; + } + + Navigator.of(context).push( + CustomPageRoute( + page: ParkingSlot(model: model), + ), + ); + } @override Widget build(BuildContext context) { + final vm = context.watch(); // عشان loading + return Scaffold( backgroundColor: AppColors.scaffoldBgColor, appBar: CustomAppBar( @@ -38,22 +59,24 @@ class _ParkingPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Parking".needTranslation, - style: TextStyle( - color: AppColors.textColor, - fontSize: 27.f, - fontWeight: FontWeight.w600)), - Container( - decoration: RoundedRectangleBorder() - .toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - // margin: EdgeInsets.all(10), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Text( + Text( + "Parking".needTranslation, + style: TextStyle( + color: AppColors.textColor, + fontSize: 27.f, + fontWeight: FontWeight.w600, + ), + ), + Container( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Text( "Dr. Sulaiman Al Habib hospital are conduction a test for the emerging corona" " virus and issuing travel certificates 24/7 in a short time and with high accuracy." " Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches " @@ -62,19 +85,20 @@ class _ParkingPageState extends State { "Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes.", style: TextStyle( color: AppColors.textColor, - fontSize: 12, height: 1.4, fontWeight: FontWeight.w500), - ), - ), - ).paddingOnly( top: 16, bottom: 16), - + fontSize: 12, + height: 1.4, + fontWeight: FontWeight.w500, + ), + ), + ), + ).paddingOnly(top: 16, bottom: 16), ], ), ), ), /// Bottom button - Container - ( + Container( decoration: RoundedRectangleBorder() .toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -93,13 +117,17 @@ class _ParkingPageState extends State { borderRadius: BorderRadius.circular(10), ), ), - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: ParkingSlot(), - ) ); - }, - child: Text( + onPressed: vm.isLoading ? null : () => _readQR(context), + child: vm.isLoading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( "Read Barcodes", style: TextStyle( fontSize: 18, @@ -116,3 +144,4 @@ class _ParkingPageState extends State { ); } } + diff --git a/lib/presentation/parking/parking_slot.dart b/lib/presentation/parking/parking_slot.dart index 094dcb7..013bb6f 100644 --- a/lib/presentation/parking/parking_slot.dart +++ b/lib/presentation/parking/parking_slot.dart @@ -1,26 +1,106 @@ - - import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:hmg_patient_app_new/features/qr_parking/models/qr_parking_response_model.dart'; +import '../../features/qr_parking/qr_parking_view_model.dart'; import '../../theme/colors.dart'; import '../../widgets/appbar/app_bar_widget.dart'; import '../../widgets/chip/app_custom_chip_widget.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:provider/provider.dart'; + class ParkingSlot extends StatefulWidget { - const ParkingSlot({super.key}); + final QrParkingResponseModel model; + + const ParkingSlot({ + super.key, + required this.model, + }); @override State createState() => _ParkingSlotState(); } class _ParkingSlotState extends State { - String? scannedCode; + + void _openDirection() { + final lat = widget.model.latitude; + final lng = widget.model.longitude; + + final valid = lat != null && + lng != null && + !(lat == 0.0 && lng == 0.0) && + lat >= -90 && lat <= 90 && + lng >= -180 && lng <= 180; + + if (!valid) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Parking location not available")), + ); + return; + } + + MapsLauncher.launchCoordinates(lat, lng); + } + + Future _resetDirection() async { + final vm = context.read(); + await vm.clearParking(); + Navigator.of(context).popUntil((route) => route.isFirst); + } + + DateTime? _parseDotNetDate(String? value) { + if (value == null || value.isEmpty) return null; + + final regExp = RegExp(r'Date\((\d+)([+-]\d+)?\)'); + final match = regExp.firstMatch(value); + if (match == null) return null; + + final milliseconds = int.tryParse(match.group(1)!); + if (milliseconds == null) return null; + + return DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true) + .toLocal(); + } + + + String _formatPrettyDate(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + + final day = date.day; + final month = months[date.month - 1]; + final year = date.year; + + return "$day $month $year"; + } + + + String _formatPrettyTime(String? value) { + final date = _parseDotNetDate(value); + if (date == null) return '-'; + + int hour = date.hour; + final minute = date.minute.toString().padLeft(2, '0'); + + final isPM = hour >= 12; + final period = isPM ? 'PM' : 'AM'; + + hour = hour % 12; + if (hour == 0) hour = 12; + + return "${hour.toString().padLeft(2, '0')}:$minute $period"; + } @override Widget build(BuildContext context) { @@ -34,7 +114,7 @@ class _ParkingSlotState extends State { body: LayoutBuilder( builder: (context, constraints) { final maxW = constraints.maxWidth; - final contentW = maxW > 600 ? 600.0 : maxW; // حد أقصى للتابلت + final contentW = maxW > 600 ? 600.0 : maxW; return Align( alignment: Alignment.topCenter, @@ -45,9 +125,11 @@ class _ParkingSlotState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Container( width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + decoration: RoundedRectangleBorder() + .toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true, @@ -58,7 +140,7 @@ class _ParkingSlotState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Parking Slot Details", + "Parking Slot Details".needTranslation, style: TextStyle( fontSize: 16.f, fontWeight: FontWeight.w600, @@ -70,10 +152,26 @@ class _ParkingSlotState extends State { spacing: 4, runSpacing: 4, children: [ - AppCustomChipWidget(labelText: "Slot: B-24".needTranslation), - AppCustomChipWidget(labelText: "Basement: Zone B".needTranslation), - AppCustomChipWidget(labelText: "Date: 16 Dec 2025".needTranslation), - AppCustomChipWidget(labelText: "Parked Since: 10:32 AM".needTranslation), + AppCustomChipWidget( + labelText: + "Slot: ${widget.model.qRParkingCode ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Basement: ${widget.model.floorDescription ?? '-'}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Date: ${_formatPrettyDate(widget.model.createdOn)}" + .needTranslation, + ), + AppCustomChipWidget( + labelText: + "Parked Since: ${_formatPrettyTime(widget.model.createdOn)}" + .needTranslation, + ), ], ), ], @@ -93,42 +191,40 @@ class _ParkingSlotState extends State { borderRadius: BorderRadius.circular(10), ), ), - onPressed: () {}, + onPressed: _openDirection, child: Text( - "Get Direction", + "Get Direction".needTranslation, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.whiteColor, ), ), ), ), - const Spacer(), - SizedBox( - width: double.infinity, - height: 48.h, - child: OutlinedButton( - style: OutlinedButton.styleFrom( - side: BorderSide(color: AppColors.primaryRedColor), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - onPressed: () { - // Reset direction logic - }, - child: Text( - "Reset Direction", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.primaryRedColor, - ), - ), - ), - ), + // const Spacer(), + // SizedBox( + // width: double.infinity, + // height: 48.h, + // child: OutlinedButton( + // style: OutlinedButton.styleFrom( + // side: BorderSide(color: AppColors.primaryRedColor), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(10), + // ), + // ), + // onPressed: _resetDirection, + // child: Text( + // "Reset Direction".needTranslation, + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.w600, + // color: AppColors.primaryRedColor, + // ), + // ), + // ), + // ), ], ), ), @@ -136,7 +232,8 @@ class _ParkingSlotState extends State { ); }, ), - ); } } + + diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index dd38180..5b18bce 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -24,12 +24,14 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_sele import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/covid19test/covid19_landing_page.dart'; import '../core/dependencies.dart'; import '../features/monthly_reports/monthly_reports_repo.dart'; import '../features/monthly_reports/monthly_reports_view_model.dart'; import '../presentation/monthly_reports/monthly_reports_page.dart'; +import '../presentation/parking/paking_page.dart'; import '../services/error_handler_service.dart'; import 'package:provider/provider.dart'; @@ -65,6 +67,7 @@ class AppRoutes { static const String userInfoSelection = '/userInfoSelection'; static const String userInfoFlowManager = '/userInfoFlowManager'; static const String monthlyReports = '/monthlyReportsPage'; + static const String qrParking = '/qrParkingPage'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -92,7 +95,6 @@ class AppRoutes { covid19Test: (context) => Covid19LandingPage(), // // healthCalculatorsPage: (context) => HealthCalculatorsPage(), - // monthlyReports: (context) => MonthlyReportsPage() monthlyReports: (context) => ChangeNotifierProvider( create: (_) => MonthlyReportsViewModel( monthlyReportsRepo: getIt(), @@ -102,6 +104,12 @@ class AppRoutes { ), healthCalculatorsPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.calculator), - healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter) - }; + healthConvertersPage: (context) => HealthCalculatorsPage(type: HealthCalConEnum.converter), + qrParking: (context) => ChangeNotifierProvider( + create: (_) => getIt(), + child: const ParkingPage(), + ), + + + }; }