From fd6b7c28031d09d0680dc5048a59cc8f0f9bf40b Mon Sep 17 00:00:00 2001 From: Faiz Hashmi Date: Wed, 22 Nov 2023 11:12:15 +0300 Subject: [PATCH] Request Module added to common --- lib/config/dependencies.dart | 4 + .../advertisment_models/ad_details_model.dart | 6 +- .../photo_special_service_scedule_model.dart | 92 +++ .../special_service_model.dart | 69 +- .../ss_photo_schedule_model.dart | 18 +- lib/repositories/appointment_repo.dart | 162 ++++ lib/repositories/common_repo.dart | 11 +- lib/repositories/provider_repo.dart | 55 ++ lib/repositories/request_repo.dart | 24 + lib/view_models/ad_view_model.dart | 35 +- lib/view_models/appointments_view_model.dart | 690 ++++++++++++++++++ .../dashboard_view_model_customer.dart | 55 ++ lib/view_models/requests_view_model.dart | 583 ++++++++------- lib/views/advertisement/ads_detail_view.dart | 130 +++- .../appointments/appointment_detail_view.dart | 245 +++++++ .../book_appointment_schedules_view.dart | 159 ++++ .../book_appointment_services_view.dart | 138 ++++ .../book_appointments_item_view.dart | 151 ++++ lib/views/appointments/pick_items_view.dart | 168 +++++ .../appointments/review_appointment_view.dart | 304 ++++++++ ...appointment_service_pick_bottom_sheet.dart | 195 +++++ .../widgets/custom_calender_widget.dart | 238 ++++++ .../customer_appointment_slider_widget.dart | 209 ++++++ .../service_item_with_price_checkbox.dart | 51 ++ pubspec.lock | 16 + pubspec.yaml | 2 + 26 files changed, 3486 insertions(+), 324 deletions(-) create mode 100644 lib/models/advertisment_models/photo_special_service_scedule_model.dart create mode 100644 lib/repositories/appointment_repo.dart create mode 100644 lib/repositories/provider_repo.dart create mode 100644 lib/view_models/appointments_view_model.dart create mode 100644 lib/view_models/dashboard_view_model_customer.dart create mode 100644 lib/views/appointments/appointment_detail_view.dart create mode 100644 lib/views/appointments/book_appointment_schedules_view.dart create mode 100644 lib/views/appointments/book_appointment_services_view.dart create mode 100644 lib/views/appointments/book_appointments_item_view.dart create mode 100644 lib/views/appointments/pick_items_view.dart create mode 100644 lib/views/appointments/review_appointment_view.dart create mode 100644 lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart create mode 100644 lib/views/appointments/widgets/custom_calender_widget.dart create mode 100644 lib/views/appointments/widgets/customer_appointment_slider_widget.dart create mode 100644 lib/views/appointments/widgets/service_item_with_price_checkbox.dart diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 32b19c1..846c2e0 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -5,8 +5,10 @@ import 'package:injector/injector.dart'; import 'package:mc_common_app/api/api_client.dart'; import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/repositories/ads_repo.dart'; +import 'package:mc_common_app/repositories/appointment_repo.dart'; import 'package:mc_common_app/repositories/common_repo.dart'; import 'package:mc_common_app/repositories/payments_repo.dart'; +import 'package:mc_common_app/repositories/provider_repo.dart'; import 'package:mc_common_app/repositories/user_repo.dart'; import 'package:mc_common_app/services/common_services.dart'; import 'package:mc_common_app/services/payments_service.dart'; @@ -29,5 +31,7 @@ class AppDependencies { injector.registerSingleton(() => AdsRepoImp()); injector.registerSingleton(() => PaymentsRepoImp()); injector.registerSingleton(() => RequestRepoImp()); + injector.registerSingleton(() => ProviderRepoImp()); + injector.registerSingleton(() => AppointmentRepoImp()); } } diff --git a/lib/models/advertisment_models/ad_details_model.dart b/lib/models/advertisment_models/ad_details_model.dart index f47b698..37dcc42 100644 --- a/lib/models/advertisment_models/ad_details_model.dart +++ b/lib/models/advertisment_models/ad_details_model.dart @@ -9,7 +9,7 @@ class AdDetailsModel { String? startdate; String? enddate; Vehicle? vehicle; - List? specialservice; + List? specialservice; // List? reserved; int? statusID; @@ -97,9 +97,9 @@ class AdDetailsModel { enddate = json['enddate']; vehicle = json['vehicle'] != null ? Vehicle.fromJson(json['vehicle']) : null; if (json['specialservice'] != null) { - specialservice = []; + specialservice = []; json['specialservice'].forEach((v) { - specialservice!.add(SpecialServiceModel.fromJson(v)); + specialservice!.add(SpecialServiceModelForAds.fromJson(v)); }); } statusID = json['statusID']; diff --git a/lib/models/advertisment_models/photo_special_service_scedule_model.dart b/lib/models/advertisment_models/photo_special_service_scedule_model.dart new file mode 100644 index 0000000..34eb8a1 --- /dev/null +++ b/lib/models/advertisment_models/photo_special_service_scedule_model.dart @@ -0,0 +1,92 @@ +class PhotoSpecialServiceScheduleModel { + int? photoOfficeID; + String? fromDate; + String? toDate; + int? photoOfficeAppointmentScheduleID; + String? photoOfficeName; + String? description; + String? areaName; + String? latitude; + String? longitude; + int? distanceKM; + int? totalItemsCount; + List? photoOfficeScheduleSlots; + + PhotoSpecialServiceScheduleModel( + {this.photoOfficeID, + this.fromDate, + this.toDate, + this.photoOfficeAppointmentScheduleID, + this.photoOfficeName, + this.description, + this.areaName, + this.latitude, + this.longitude, + this.distanceKM, + this.totalItemsCount, + this.photoOfficeScheduleSlots}); + + PhotoSpecialServiceScheduleModel.fromJson(Map json) { + photoOfficeID = json['photoOfficeID']; + fromDate = json['fromDate']; + toDate = json['toDate']; + photoOfficeAppointmentScheduleID = json['photoOfficeAppointmentScheduleID']; + photoOfficeName = json['photoOfficeName']; + description = json['description']; + areaName = json['areaName']; + latitude = json['latitude']; + longitude = json['longitude']; + distanceKM = json['distanceKM']; + totalItemsCount = json['totalItemsCount']; + if (json['photoOfficeScheduleSlots'] != null) { + photoOfficeScheduleSlots = []; + json['photoOfficeScheduleSlots'].forEach((v) { + photoOfficeScheduleSlots!.add(PhotoOfficeScheduleSlots.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['photoOfficeID'] = photoOfficeID; + data['fromDate'] = fromDate; + data['toDate'] = toDate; + data['photoOfficeAppointmentScheduleID'] = photoOfficeAppointmentScheduleID; + data['photoOfficeName'] = photoOfficeName; + data['description'] = description; + data['areaName'] = areaName; + data['latitude'] = latitude; + data['longitude'] = longitude; + data['distanceKM'] = distanceKM; + data['totalItemsCount'] = totalItemsCount; + if (photoOfficeScheduleSlots != null) { + data['photoOfficeScheduleSlots'] = photoOfficeScheduleSlots!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class PhotoOfficeScheduleSlots { + int? id; + String? slotDate; + String? startTime; + String? endTime; + + PhotoOfficeScheduleSlots({this.id, this.slotDate, this.startTime, this.endTime}); + + PhotoOfficeScheduleSlots.fromJson(Map json) { + id = json['id']; + slotDate = json['slotDate']; + startTime = json['startTime']; + endTime = json['endTime']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['slotDate'] = slotDate; + data['startTime'] = startTime; + data['endTime'] = endTime; + return data; + } +} diff --git a/lib/models/advertisment_models/special_service_model.dart b/lib/models/advertisment_models/special_service_model.dart index 59affc4..33e21c5 100644 --- a/lib/models/advertisment_models/special_service_model.dart +++ b/lib/models/advertisment_models/special_service_model.dart @@ -15,18 +15,18 @@ class SpecialServiceModel { SpecialServiceModel( {this.id, - this.name, - this.description, - this.price, - this.specialServiceType, - this.specialServiceTypeName, - this.isActive, - this.startDate, - this.endDate, - this.details, - this.office, - this.isSelected, - this.isDelivery}); + this.name, + this.description, + this.price, + this.specialServiceType, + this.specialServiceTypeName, + this.isActive, + this.startDate, + this.endDate, + this.details, + this.office, + this.isSelected, + this.isDelivery}); SpecialServiceModel.fromJson(Map json) { id = json['id']; @@ -83,8 +83,7 @@ class Details { int? tocity; int? price; - Details( - {this.id, this.specialServiceID, this.fromcity, this.tocity, this.price}); + Details({this.id, this.specialServiceID, this.fromcity, this.tocity, this.price}); Details.fromJson(Map json) { id = json['id']; @@ -113,13 +112,7 @@ class Office { String? city; int? specialServiceID; - Office( - {this.id, - this.officeAreaName, - this.officeAreaNameN, - this.cityID, - this.city, - this.specialServiceID}); + Office({this.id, this.officeAreaName, this.officeAreaNameN, this.cityID, this.city, this.specialServiceID}); Office.fromJson(Map json) { id = json['id']; @@ -141,3 +134,37 @@ class Office { return data; } } + +class SpecialServiceModelForAds { + int? adsID; + int? specialServiceID; + String? name; + String? description; + double? price; + + SpecialServiceModelForAds({ + this.adsID, + this.specialServiceID, + this.name, + this.description, + this.price, + }); + + SpecialServiceModelForAds.fromJson(Map json) { + adsID = json['adsID']; + specialServiceID = json['specialServiceID']; + name = json['name']; + description = json['description']; + price = json['price']; + } + + Map toJson() { + final Map data = {}; + data['adsID'] = adsID; + data['specialServiceID'] = specialServiceID; + data['name'] = name; + data['description'] = description; + data['price'] = price; + return data; + } +} diff --git a/lib/models/advertisment_models/ss_photo_schedule_model.dart b/lib/models/advertisment_models/ss_photo_schedule_model.dart index 468c6fb..d863f41 100644 --- a/lib/models/advertisment_models/ss_photo_schedule_model.dart +++ b/lib/models/advertisment_models/ss_photo_schedule_model.dart @@ -73,15 +73,27 @@ class PhotoOfficeScheduleSlots { String? slotDate; String? startTime; String? endTime; + int? bookAppointment; + int? allowAppointment; + int? slotDurationMinute; PhotoOfficeScheduleSlots( - {this.id, this.slotDate, this.startTime, this.endTime}); + {this.id, + this.slotDate, + this.startTime, + this.endTime, + this.bookAppointment, + this.allowAppointment, + this.slotDurationMinute}); PhotoOfficeScheduleSlots.fromJson(Map json) { id = json['id']; slotDate = json['slotDate']; startTime = json['startTime']; endTime = json['endTime']; + bookAppointment = json['bookAppointment']; + allowAppointment = json['allowAppointment']; + slotDurationMinute = json['slotDurationMinute']; } Map toJson() { @@ -90,6 +102,10 @@ class PhotoOfficeScheduleSlots { data['slotDate'] = slotDate; data['startTime'] = startTime; data['endTime'] = endTime; + data['bookAppointment'] = bookAppointment; + data['allowAppointment'] = allowAppointment; + data['slotDurationMinute'] = slotDurationMinute; return data; } } + diff --git a/lib/repositories/appointment_repo.dart b/lib/repositories/appointment_repo.dart new file mode 100644 index 0000000..e3f5236 --- /dev/null +++ b/lib/repositories/appointment_repo.dart @@ -0,0 +1,162 @@ +import 'dart:developer'; + +import 'package:mc_common_app/api/api_client.dart'; +import 'package:mc_common_app/classes/app_state.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/config/dependencies.dart'; +import 'package:mc_common_app/models/generic_resp_model.dart'; +import 'package:mc_common_app/models/m_response.dart'; +import 'package:mc_common_app/models/provider_branches_models/profile/services.dart'; +import 'package:mc_common_app/models/schedule_model.dart'; +import 'package:mc_common_app/models/service_schedule_model.dart'; +import 'package:mc_common_app/utils/enums.dart'; + +abstract class AppointmentRepo { + Future getAllServices(String branchId); + + Future createSchedule(Map map); + + Future addServicesInSchedule(Map map); + + Future updateSchedule(Map map); + + Future> getSchedules(String branchId); + + Future updateServicesInSchedule(Map map); + + Future> mergeServiceIntoAvailableSchedules({ + required List serviceItemIdsForHome, + required List serviceItemIdsForWorkshop, + }); + + Future createServiceAppointment({required List schedules, required int serviceProviderID}); + + Future cancelOrRescheduleServiceAppointment({required int serviceAppointmentID, required int serviceSlotID, required int appointmentScheduleAction}); +} + +class AppointmentRepoImp implements AppointmentRepo { + @override + Future getAllServices(String branchId) async { + Map map = {"ProviderBranchID": branchId}; + String t = AppState().getUser.data!.accessToken ?? ""; + return await injector.get().getJsonForObject((json) => Services.fromJson(json), ApiConsts.getServicesOfBranch, token: t, queryParameters: map); + } + + @override + Future createSchedule(Map map) async { + String t = AppState().getUser.data!.accessToken ?? ""; + return await injector.get().postJsonForObject((json) => MResponse.fromJson(json), ApiConsts.createSchedule, map, token: t); + } + + @override + Future addServicesInSchedule(Map map) async { + String t = AppState().getUser.data!.accessToken ?? ""; + return await injector.get().postJsonForObject((json) => MResponse.fromJson(json), ApiConsts.createGroup, map, token: t); + } + + @override + Future> getSchedules(String branchId) async { + Map map = {"ServiceProviderBranchID": branchId}; + String t = AppState().getUser.data!.accessToken ?? ""; + + GenericRespModel adsGenericModel = await injector.get().getJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.getSchedule, + token: t, + queryParameters: map, + ); + + return List.generate(adsGenericModel.data.length, (index) => ScheduleData.fromJson(adsGenericModel.data[index])); + } + + @override + Future updateSchedule(Map map) async { + String t = AppState().getUser.data!.accessToken ?? ""; + return await injector.get().postJsonForObject((json) => MResponse.fromJson(json), ApiConsts.updateSchedule, map, token: t); + } + + @override + Future updateServicesInSchedule(Map map) async { + String t = AppState().getUser.data!.accessToken ?? ""; + return await injector.get().postJsonForObject((json) => MResponse.fromJson(json), ApiConsts.updateGroup, map, token: t); + } + + Future> mergeServiceIntoAvailableSchedules({ + required List serviceItemIdsForHome, + required List serviceItemIdsForWorkshop, + }) async { + String t = AppState().getUser.data!.accessToken ?? ""; + var queryParameters = [ + { + "appointmentType": 2, + "ServiceItemIDs": serviceItemIdsForHome, + }, + { + "appointmentType": 1, + "ServiceItemIDs": serviceItemIdsForWorkshop, + } + ]; + GenericRespModel adsGenericModel = await injector.get().postJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.GetServiceItemAppointmentScheduleSlots, + queryParameters, + token: t, + ); + if (adsGenericModel.data == null) { + return []; + } + List serviceAppointmentScheduleModel = + List.generate(adsGenericModel.data.length, (index) => ServiceAppointmentScheduleModel.fromJson(adsGenericModel.data[index], isForAppointment: true)); + return serviceAppointmentScheduleModel; + } + + Future createServiceAppointment({required List schedules, required int serviceProviderID}) async { + String t = AppState().getUser.data!.accessToken ?? ""; + int customerId = AppState().getUser.data!.userInfo!.customerId ?? 0; + + List> mapList = []; + schedules.forEach((schedule) { + List serviceItemIds = []; + schedule.servicesListInAppointment!.forEach((service) { + service.serviceItems!.forEach((item) { + serviceItemIds.add(item.id!); + }); + }); + mapList.add({ + "serviceSlotID": schedule.selectedCustomTimeDateSlotModel!.date!.slotId, + "serviceProviderID": serviceProviderID, + "customerID": customerId, + "serviceItemID": serviceItemIds, + }); + }); + + GenericRespModel adsGenericModel = await injector.get().postJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.ServiceProvidersAppointmentCreate, + mapList, + token: t, + ); + + return adsGenericModel; + } + + @override + Future cancelOrRescheduleServiceAppointment({required int serviceAppointmentID, required int serviceSlotID, required int appointmentScheduleAction}) async { + String t = AppState().getUser.data!.accessToken ?? ""; + + final payload = { + "serviceAppointmentID": serviceAppointmentID, + "serviceSlotID": serviceSlotID, + "appointmentScheduleAction": appointmentScheduleAction, + }; + + GenericRespModel adsGenericModel = await injector.get().postJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.ServiceProviderAppointmentRescheduleCancelAppointment, + payload, + token: t, + ); + + return adsGenericModel; + } +} diff --git a/lib/repositories/common_repo.dart b/lib/repositories/common_repo.dart index 439fa21..e4e24d7 100644 --- a/lib/repositories/common_repo.dart +++ b/lib/repositories/common_repo.dart @@ -27,7 +27,7 @@ abstract class CommonRepo { Future getCarCheckServiceScheduleDetails({required double lat, required double long}); - Future getPhotographyServiceScheduleDetails({required double lat, required double long}); + Future> getPhotographyServiceScheduleListByOffices({required double lat, required double long}); // Future> getProviderServiceCategories(); @@ -95,7 +95,7 @@ class CommonRepoImp implements CommonRepo { } @override - Future getPhotographyServiceScheduleDetails({required double lat, required double long}) async { + Future> getPhotographyServiceScheduleListByOffices({required double lat, required double long}) async { var params = { "Latitude": lat.toString(), "Longitude": long.toString(), @@ -106,8 +106,11 @@ class CommonRepoImp implements CommonRepo { queryParameters: params, ApiConsts.adsPhotoOfficeAppointmentScheduleSlotGet, ); - SSPhotoScheduleModel ssPhotoScheduleModel = SSPhotoScheduleModel.fromJson(genericRespModel.data[0]); - return ssPhotoScheduleModel; + if (genericRespModel.data == null) { + return []; + } + List ssPhotoScheduleModel = List.generate(genericRespModel.data.length, (index) => SSPhotoScheduleModel.fromJson(genericRespModel.data[index])); + return ssPhotoScheduleModel ?? []; } @override diff --git a/lib/repositories/provider_repo.dart b/lib/repositories/provider_repo.dart new file mode 100644 index 0000000..fee2cc1 --- /dev/null +++ b/lib/repositories/provider_repo.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:mc_common_app/api/api_client.dart'; +import 'package:mc_common_app/classes/app_state.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/config/dependencies.dart'; +import 'package:mc_common_app/models/generic_resp_model.dart'; +import 'package:mc_common_app/models/provider_branches_models/branch_detail_model.dart'; +import 'package:mc_common_app/models/provider_branches_models/provider_profile_model.dart'; +import 'package:mc_common_app/models/services/item_model.dart'; + +abstract class ProviderRepo { + Future> getAllNearBranchAndServices(); + + Future> getServiceItems(int serviceId); + + Future getBranchAndServices(int providerId); +} + +class ProviderRepoImp implements ProviderRepo { + ApiClient apiClient = injector.get(); + AppState appState = injector.get(); + + @override + Future> getAllNearBranchAndServices() async { + GenericRespModel adsGenericModel = await apiClient.getJsonForObject((json) => GenericRespModel.fromJson(json), ApiConsts.GetAllNearBranches, token: appState.getUser.data!.accessToken); + List nearBranches = List.generate(adsGenericModel.data.length, (index) => BranchDetailModel.fromJson(adsGenericModel.data[index])); + return nearBranches; + } + + @override + Future> getServiceItems(int serviceId) async { + var queryParameters = { + "ServiceProviderServiceID": serviceId.toString(), + }; + + GenericRespModel adsGenericModel = await apiClient.getJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.getServiceItems, + token: appState.getUser.data!.accessToken, + queryParameters: queryParameters, + ); + List serviceItems = List.generate(adsGenericModel.data.length, (index) => ItemData.fromJson(adsGenericModel.data[index])); + return serviceItems; + } + + @override + Future getBranchAndServices(int providerId) async { + var postParams = {"serviceProviderID": providerId.toString()}; + GenericRespModel adsGenericModel = + await apiClient.getJsonForObject((json) => GenericRespModel.fromJson(json), ApiConsts.BranchesAndServices, token: appState.getUser.data!.accessToken, queryParameters: postParams); + + return ProviderProfileModel.fromJson(adsGenericModel.data); + } +} diff --git a/lib/repositories/request_repo.dart b/lib/repositories/request_repo.dart index 6365983..2b8de66 100644 --- a/lib/repositories/request_repo.dart +++ b/lib/repositories/request_repo.dart @@ -3,11 +3,14 @@ import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/config/dependencies.dart'; import 'package:mc_common_app/models/generic_resp_model.dart'; +import 'package:mc_common_app/models/requests/offers_model.dart'; import 'package:mc_common_app/models/requests/request_model.dart'; abstract class RequestRepo { Future createRequest(Map map); + Future> getOffersByRequest({required int requestId, int serviceProviderId = 0}); + Future> getRequests(Map postParams); } @@ -42,4 +45,25 @@ class RequestRepoImp implements RequestRepo { ); return requests; } + + @override + Future> getOffersByRequest({required int requestId, int serviceProviderId = 0}) async { + var queryParameters = { + "RequestID": requestId.toString(), + "ServiceProviderID": serviceProviderId.toString(), + }; + GenericRespModel genericRespModel = await apiClient.getJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.getRequestOffers, + queryParameters: queryParameters, + token: appState.getUser.data!.accessToken, + ); + List offersList = List.generate( + genericRespModel.data.length, + (index) => OffersModel.fromJson( + genericRespModel.data[index], + ), + ); + return offersList; + } } diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index 38f5634..76aa86f 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -995,8 +995,8 @@ class AdVM extends BaseVM { } void pickMultipleImages() async { - List Images = await commonServices.pickMultipleImages(); - pickedVehicleImages.addAll(Images); + List images = await commonServices.pickMultipleImages(); + pickedVehicleImages.addAll(images); if (pickedVehicleImages.isNotEmpty) vehicleImageError = ""; notifyListeners(); } @@ -1054,8 +1054,8 @@ class AdVM extends BaseVM { } void pickMultipleDamageImages() async { - List Images = await commonServices.pickMultipleImages(); - pickedDamageImages.addAll(Images); + List images = await commonServices.pickMultipleImages(); + pickedDamageImages.addAll(images); if (pickedDamageImages.isNotEmpty) vehicleDamageImageError = ""; notifyListeners(); } @@ -1097,6 +1097,28 @@ class AdVM extends BaseVM { notifyListeners(); } + SelectionModel photoOfficeSelectedId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updatePhotoOfficeSelectedId(SelectionModel id) { + photoOfficeSelectedId = id; + notifyListeners(); + } + + List photoSSSchedulesByOffices = []; + + Future getPhotographyServiceScheduleListByOffices({required double latitude, required double longitude, bool isNeedToRebuild = false}) async { + if (isNeedToRebuild) setState(ViewState.busy); + + try { + photoSSSchedulesByOffices = await commonRepo.getPhotographyServiceScheduleListByOffices(lat: latitude, long: longitude); + } catch (e) { + setState(ViewState.busy); + Utils.showToast("Error: ${e.toString()}"); + } + } + + + Future createNewAd() async { AppState appState = injector.get(); List adsSelectedServices = []; @@ -1111,9 +1133,10 @@ class AdVM extends BaseVM { specialServiceIDs: adsSelectedServices, ); List vehicleImages = []; - pickedVehicleImages.forEach((element) async { + + for (var element in pickedVehicleImages) { vehicleImages.add(await convertFileToVehiclePostingImages(file: element)); - }); + } List vehicleDamageImages = []; diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart new file mode 100644 index 0000000..1031175 --- /dev/null +++ b/lib/view_models/appointments_view_model.dart @@ -0,0 +1,690 @@ +import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/config/routes.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart'; +import 'package:mc_common_app/models/enums_model.dart'; +import 'package:mc_common_app/models/generic_resp_model.dart'; +import 'package:mc_common_app/models/provider_branches_models/branch_detail_model.dart'; +import 'package:mc_common_app/models/provider_branches_models/provider_profile_model.dart'; +import 'package:mc_common_app/models/service_schedule_model.dart'; +import 'package:mc_common_app/models/services/item_model.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; +import 'package:mc_common_app/models/widgets_models.dart'; +import 'package:mc_common_app/repositories/appointment_repo.dart'; +import 'package:mc_common_app/repositories/common_repo.dart'; +import 'package:mc_common_app/repositories/provider_repo.dart'; +import 'package:mc_common_app/services/common_services.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/utils/enums.dart'; +import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/utils/utils.dart'; +import 'package:mc_common_app/view_models/base_view_model.dart'; +import 'package:mc_common_app/view_models/dashboard_view_model_customer.dart'; +import 'package:mc_common_app/view_models/payment_view_model.dart'; +import 'package:mc_common_app/views/appointments/book_appointment_schedules_view.dart'; +import 'package:mc_common_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; +import 'package:mc_common_app/widgets/common_widgets/info_bottom_sheet.dart'; +import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; + +class AppointmentsVM extends BaseVM { + final CommonRepo commonRepo; + final CommonAppServices commonServices; + final ProviderRepo providerRepo; + final AppointmentRepo scheduleRepo; + + AppointmentsVM({required this.commonServices, required this.scheduleRepo, required this.providerRepo, required this.commonRepo}); + + bool isFetchingLists = false; + + List myAppointments = []; + List myUpComingAppointments = []; + List myFilteredAppointments = []; + List appointmentsFilterOptions = []; + + // List availableSchedules = []; + + bool isFetchingServices = false; + + List branchCategories = []; + + bool isHomeTapped = false; + + List serviceAppointmentScheduleList = []; + + bool ifItemAlreadySelected(int id) { + int indexFound = allSelectedItemsInAppointments.indexWhere((element) => element.id == id); + if (indexFound != -1) { + return true; + } + return false; + } + + List allSelectedItemsInAppointments = []; + + Future onItemsSelectedInService() async { + if (currentServiceSelection != null) { + int index = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); + + if (index == -1) { + double totalPrice = 0.0; + currentServiceSelection!.serviceItems!.forEach((element) { + totalPrice = totalPrice + double.parse(element.price ?? "0.0"); + }); + currentServiceSelection!.currentTotalServicePrice = totalPrice; + servicesInCurrentAppointment.insert(0, currentServiceSelection!); + } + + resetCategorySelectionBottomSheet(); + notifyListeners(); + } + } + + Future onBookAppointmentPressed(BuildContext context) async { + Utils.showLoading(context); + bool isSuccess = false; + List appointmentIdsList = []; + try { + GenericRespModel genericRespModel = await scheduleRepo.createServiceAppointment( + schedules: serviceAppointmentScheduleList, + serviceProviderID: selectedBranchModel!.serviceProviderId ?? 0, + ); + + if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { + Utils.hideLoading(context); + Utils.showToast("${genericRespModel.message.toString()}"); + return; + } + if (genericRespModel.data != null) { + genericRespModel.data.forEach((element) { + if (element['appointmentID'] != 0) { + appointmentIdsList.add(element['appointmentID']); + isSuccess = true; + } else { + isSuccess = false; + return; + } + }); + } + + context.read().onNavbarTapped(1); + applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.booked); + Utils.hideLoading(context); + resetAfterBookingAppointment(); + if (isSuccess) { + if (amountToPayForAppointment > 0) { + context.read().updateAppointmentIdsForPayment(ids: appointmentIdsList); + navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.appointment); + } else { + Utils.showToast("Your appointment has been booked successfully!"); + getMyAppointments(); + navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); + } + } + } catch (e) { + Utils.showToast("${e.toString()}"); + } + } + + Future onConfirmAppointmentPressed({required BuildContext context, required appointmentId}) async { + context.read().updateAppointmentIdsForPayment(ids: [appointmentId]); + navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.appointment); + } + + Future onCancelAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async { + Utils.showLoading(context); + try { + GenericRespModel genericRespModel = await scheduleRepo.cancelOrRescheduleServiceAppointment( + serviceAppointmentID: appointmentListModel.id ?? 0, + serviceSlotID: appointmentListModel.serviceSlotID ?? 0, + appointmentScheduleAction: 2, // 1 for Reschedule and 2 for Cancel + ); + + if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { + Utils.hideLoading(context); + Utils.showToast("${genericRespModel.message.toString()}"); + return; + } + if (genericRespModel.data == 1) { + context.read().onNavbarTapped(1); + applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.cancelled); + Utils.showToast("${genericRespModel.message.toString()}"); + await getMyAppointments(); + Utils.hideLoading(context); + getMyAppointments(); + navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); + } + } catch (e) { + Utils.showToast("${e.toString()}"); + } + } + + void updateIsHomeTapped(bool value) { + isHomeTapped = value; + if (currentServiceSelection != null) { + currentServiceSelection!.isHomeSelected = value; + } + notifyListeners(); + } + + String pickedHomeLocation = ""; + + void updatePickedHomeLocation(String value) { + pickedHomeLocation = value; + pickHomeLocationError = ""; + if (currentServiceSelection != null) { + currentServiceSelection!.homeLocation = value; + } + notifyListeners(); + } + + SelectionModel branchSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updateProviderCategoryId(SelectionModel id) { + branchSelectedCategoryId = id; + getBranchServices(categoryId: branchSelectedCategoryId.selectedId); + notifyListeners(); + } + + List providersFilterOptions = []; + List nearbyBranches = []; + BranchDetailModel? selectedBranchModel; + + List branchServices = []; + + List servicesInCurrentAppointment = []; + ServiceModel? currentServiceSelection; + + void updateBranchServiceId(SelectionModel id) async { + branchSelectedServiceId = id; + currentServiceSelection = branchServices.firstWhere((element) => element.serviceProviderServiceId == id.selectedId); + + notifyListeners(); + } + + void removeServiceInCurrentAppointment(int index) { + int serviceId = servicesInCurrentAppointment.elementAt(index).serviceProviderServiceId ?? -1; + allSelectedItemsInAppointments.removeWhere((element) => element.serviceProviderServiceId == serviceId); + servicesInCurrentAppointment.removeAt(index); + notifyListeners(); + } + + resetCategorySelectionBottomSheet() { + selectedSubServicesCounter = 0; + branchSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + isHomeTapped = false; + branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + currentServiceSelection = null; + } + + resetAfterBookingAppointment() { + allSelectedItemsInAppointments.clear(); + servicesInCurrentAppointment.clear(); + serviceAppointmentScheduleList.clear(); + } + + List myAppointmentsEnum = []; + + populateAppointmentsFilterList() async { + appointmentsFilterOptions.clear(); + + myAppointmentsEnum = await commonRepo.getEnumTypeValues(enumTypeID: 13); //TODO: 13 is to get Appointments Filter Enums + + for (int i = 0; i < myAppointmentsEnum.length; i++) { + appointmentsFilterOptions.add(FilterListModel(title: myAppointmentsEnum[i].enumValueStr, isSelected: false, id: myAppointmentsEnum[i].enumValue)); + } + appointmentsFilterOptions.insert(0, FilterListModel(title: "All Appointments", isSelected: true, id: 0)); + + notifyListeners(); + } + + applyFilterOnAppointmentsVM({required AppointmentStatusEnum appointmentStatusEnum}) { + if (appointmentsFilterOptions.isEmpty) return; + for (var value in appointmentsFilterOptions) { + value.isSelected = false; + } + appointmentsFilterOptions[appointmentStatusEnum.getIdFromAppointmentStatusEnum()].isSelected = true; + + if (appointmentStatusEnum.getIdFromAppointmentStatusEnum() == 0) { + myFilteredAppointments = myAppointments; + notifyListeners(); + return; + } + + myFilteredAppointments = myAppointments.where((element) => element.appointmentStatusID! == appointmentStatusEnum.getIdFromAppointmentStatusEnum()).toList(); + notifyListeners(); + } + + Future getMyAppointments({bool isNeedToRebuild = false}) async { + if (isNeedToRebuild) setState(ViewState.busy); + + myAppointments = await commonRepo.getMyAppointments(); + myFilteredAppointments = myAppointments; + myUpComingAppointments = myAppointments.where((element) => element.appointmentStatusEnum == AppointmentStatusEnum.booked).toList(); + setState(ViewState.idle); + // applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.allAppointments); + notifyListeners(); + } + + updateSelectedBranch(BranchDetailModel branchDetailModel) { + selectedBranchModel = branchDetailModel; + getBranchCategories(); + notifyListeners(); + } + + updateSelectedAppointmentDate({required int dateIndex, required int scheduleIndex}) { + serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList!.forEach((element) { + element.date!.isSelected = false; + }); + + serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.isSelected = true; + serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex = dateIndex; + final date = TimeSlotModel( + date: serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.date, + slotId: serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.slotId, + isSelected: true, + slot: "", + ); + serviceAppointmentScheduleList[scheduleIndex].selectedCustomTimeDateSlotModel = CustomTimeDateSlotModel(date: date); + notifyListeners(); + } + + updateSelectedAppointmentSlotByDate({required int scheduleIndex, required int slotIndex}) { + serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList!.forEach((element) { + element.availableSlots!.forEach((element) => element.isSelected = false); + }); + int index = serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex!; + serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots![slotIndex].isSelected = true; + serviceAppointmentScheduleList[scheduleIndex].selectedCustomTimeDateSlotModel!.availableSlots = serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots!; + notifyListeners(); + } + + double amountToPayForAppointment = 0.0; + double totalAmount = 0.0; + + List serviceItemsFromApi = []; + + ProviderProfileModel? providerProfileModel; + + int selectedSubServicesCounter = 0; + + onItemUpdateOrSelected(int index, bool selected, int itemId) { + int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); + if (serviceIndex == -1) { + return; + } + + serviceItemsFromApi[index].isUpdateOrSelected = selected; + serviceItemsFromApi[index].isHomeSelected = isHomeTapped; + if (selected) { + selectedSubServicesCounter = selectedSubServicesCounter + 1; + selectSubServicesError = ""; + currentServiceSelection!.serviceItems!.add(serviceItemsFromApi[index]); + allSelectedItemsInAppointments.add(serviceItemsFromApi[index]); + allSelectedItemsInAppointments.forEach((element) { + if (!ifItemAlreadySelected(element.id!)) { + servicesInCurrentAppointment[serviceIndex].serviceItems!.add(serviceItemsFromApi[index]); + servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = + servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice + double.parse((serviceItemsFromApi[index].price) ?? "0.0"); + } + }); + } + if (!selected) { + selectedSubServicesCounter = selectedSubServicesCounter - 1; + currentServiceSelection!.serviceItems!.removeWhere((element) => element.id == itemId); + allSelectedItemsInAppointments.removeWhere((element) => element.id == itemId); + servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = + servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice - double.parse((serviceItemsFromApi[index].price) ?? "0.0"); + servicesInCurrentAppointment[serviceIndex].serviceItems!.removeWhere((element) => element.id == itemId); + } + notifyListeners(); + } + + populateBranchesFilterList() { + providersFilterOptions.clear(); + providersFilterOptions = [ + FilterListModel(title: "All Providers", isSelected: true, id: 0), + FilterListModel(title: "Maintenance", isSelected: false, id: 1), + FilterListModel(title: "Oil Service", isSelected: false, id: 2), + FilterListModel(title: "Accessories", isSelected: false, id: 3), + FilterListModel(title: "Tire Service", isSelected: false, id: 4), + FilterListModel(title: "Dent and Paint", isSelected: false, id: 5), + ]; + notifyListeners(); + } + + applyFilterOnProviders({required int index}) { + if (providersFilterOptions.isEmpty) return; + for (var value in providersFilterOptions) { + value.isSelected = false; + } + providersFilterOptions[index].isSelected = true; + notifyListeners(); + } + + getAllNearBranches({bool isNeedToRebuild = false}) async { + //TODO: needs to lat,long into API + nearbyBranches.clear(); + if (isNeedToRebuild) setState(ViewState.busy); + nearbyBranches = await providerRepo.getAllNearBranchAndServices(); + setState(ViewState.idle); + } + + Future> getServiceItems(int serviceId) async { + serviceItemsFromApi.clear(); + serviceItemsFromApi = await providerRepo.getServiceItems(serviceId); + serviceItemsFromApi.forEach((item) { + if (ifItemAlreadySelected(item.id!)) { + item.isUpdateOrSelected = true; + } + }); + setState(ViewState.idle); + return serviceItemsFromApi; + } + + getBranchAndServices(int providerId) async { + providerProfileModel = null; + providerProfileModel = await providerRepo.getBranchAndServices(providerId); + setState(ViewState.idle); + } + + String pickHomeLocationError = ""; + String selectSubServicesError = ""; + + SelectionModel branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + bool isCategoryAlreadyPresent(int id) { + final contain = branchCategories.where((element) => element.id == id); + if (contain.isEmpty) { + return false; + } + return true; + } + + void getBranchCategories() async { + for (var value in selectedBranchModel!.branchServices!) { + if (!isCategoryAlreadyPresent(value.categoryId!)) { + branchCategories.add(DropValue(value.categoryId!, value.categoryName!, "")); + } + } + notifyListeners(); + } + + getBranchServices({required int categoryId}) async { + branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + isHomeTapped = false; + pickedHomeLocation = ""; + pickHomeLocationError = ""; + if (categoryId != -1) { + isFetchingServices = true; + branchServices = getFilteredBranchServices(categoryId: categoryId); + isFetchingServices = false; + notifyListeners(); + } + } + + List getFilteredBranchServices({required int categoryId}) { + List filteredServices = selectedBranchModel!.branchServices!.where((element) => element.categoryId == categoryId).toList(); + return filteredServices; + } + + void updatePickHomeLocationError(String value) { + pickHomeLocationError = value; + // notifyListeners(); + } + + bool isServiceSelectionValidated() { + if (branchSelectedServiceId.selectedId == -1) { + return false; + } + + if (isHomeTapped) { + if (pickedHomeLocation == "") { + updatePickHomeLocationError(GlobalConsts.homeLocationEmptyError); + return false; + } + } + return true; + } + + bool validateItemsSelection() { + for (var value in serviceItemsFromApi) { + if (value.isUpdateOrSelected!) { + return true; + } + } + selectSubServicesError = "Please select at least one sub service"; + notifyListeners(); + return false; + } + + String getTotalPrice(List serviceItems) { + var totalPrice = 0.0; + serviceItems.forEach((element) { + totalPrice = totalPrice + (element.currentTotalServicePrice); + }); + return totalPrice.toString(); + } + + void openTheAddServiceBottomSheet(BuildContext context, AppointmentsVM appointmentsVM) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: true, + builder: (BuildContext context) { + return AppointmentServicePickBottomSheet(); + }, + ); + } + + void priceBreakDownClicked(BuildContext context, ServiceModel selectedService) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: true, + builder: (BuildContext context) { + double totalKms = 15.3; + return InfoBottomSheet( + title: "Charges Breakdown".toText(fontSize: 24, isBold: true), + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Services".toText(fontSize: 16, isBold: true), + Column( + children: List.generate( + selectedService.serviceItems!.length, + (index) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${selectedService.serviceItems![index].name}".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "${selectedService.serviceItems![index].price} SAR".toText(fontSize: 12, isBold: true), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + "${selectedService.currentTotalServicePrice} SAR".toText(fontSize: 16, isBold: true), + ], + ), + if (selectedService.isHomeSelected) ...[ + 20.height, + "Home Location".toText(fontSize: 16, isBold: true), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${totalKms}km ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "${selectedService.rangePricePerKm} x $totalKms".toText(fontSize: 12, isBold: true), + ], + ), + 8.height, + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + "${selectedService.rangePricePerKm ?? 0 * totalKms} SAR".toText(fontSize: 16, isBold: true), + ], + ), + ], + 30.height, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Total Amount ".toText(fontSize: 16, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + (selectedService.isHomeSelected + ? "${(selectedService.currentTotalServicePrice) + (double.parse((selectedService.rangePricePerKm ?? "0.0")) * totalKms)}" + : "${selectedService.currentTotalServicePrice}") + .toText(fontSize: 29, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), + ], + ) + ], + ), + 30.height, + ], + )); + }); + } + + void onReviewButtonPressed(BuildContext context) { + bool isValidated = false; + + for (int i = 0; i < serviceAppointmentScheduleList.length; i++) { + final schedule = serviceAppointmentScheduleList[i]; + if (schedule.selectedCustomTimeDateSlotModel == null) { + isValidated = false; + break; + } + if (schedule.selectedCustomTimeDateSlotModel!.date == null || !schedule.selectedCustomTimeDateSlotModel!.date!.isSelected) { + isValidated = false; + break; + } else { + if (schedule.selectedCustomTimeDateSlotModel!.availableSlots == null) { + isValidated = true; + break; + } else { + TimeSlotModel slot = schedule.selectedCustomTimeDateSlotModel!.availableSlots!.firstWhere((element) => element.isSelected); + if (slot.date.isNotEmpty) { + isValidated = true; + break; + } + } + } + } + if (!isValidated) { + Utils.showToast("You must select appointment time for each schedule's appointment."); + return; + } + navigateWithName(context, AppRoutes.reviewAppointmentView); + } + + void onServicesNextPressed(BuildContext context) async { + Utils.showLoading(context); + + List serviceItemIdsForHome = []; + List serviceItemIdsForWorkshop = []; + allSelectedItemsInAppointments.forEach((serviceItem) { + if (serviceItem.isHomeSelected!) { + serviceItemIdsForHome.add(serviceItem.id!.toString()); + } else { + serviceItemIdsForWorkshop.add(serviceItem.id!.toString()); + } + }); + + serviceAppointmentScheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules( + serviceItemIdsForHome: serviceItemIdsForHome, + serviceItemIdsForWorkshop: serviceItemIdsForWorkshop, + ); + + if (serviceAppointmentScheduleList.isEmpty) { + Utils.hideLoading(context); + Utils.showToast("There are no available appointments for selected Items."); + return; + } + totalAmount = 0.0; + amountToPayForAppointment = 0.0; + serviceAppointmentScheduleList.forEach( + (schedule) { + amountToPayForAppointment = amountToPayForAppointment + (schedule.amountToPay ?? 0.0); + totalAmount = totalAmount + (schedule.amountTotal ?? 0.0); + }, + ); + Utils.hideLoading(context); + + navigateWithName(context, AppRoutes.bookAppointmenSchedulesView, arguments: ScreenArgumentsForAppointmentDetailPage(routeFlag: 1, appointmentId: 0)); // 1 For Creating an Appointment + + notifyListeners(); + } + + Future onRescheduleAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async { + Utils.showLoading(context); + + List serviceItemIdsForHome = []; + List serviceItemIdsForWorkshop = []; + appointmentListModel.appointmentServicesList!.forEach((service) { + service.serviceItems!.forEach((serviceItem) { + serviceItemIdsForWorkshop.add(serviceItem.id!.toString()); + + // if (serviceItem.isHomeSelected ?? false) { + // serviceItemIdsForHome.add(serviceItem.id!.toString()); + // } else { + // serviceItemIdsForWorkshop.add(serviceItem.id!.toString()); + // } + }); + }); + + serviceAppointmentScheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules( + serviceItemIdsForHome: serviceItemIdsForHome, + serviceItemIdsForWorkshop: serviceItemIdsForWorkshop, + ); + + if (serviceAppointmentScheduleList.isEmpty) { + Utils.hideLoading(context); + Utils.showToast("There are no available appointments for selected Items."); + return; + } + Utils.hideLoading(context); + + navigateWithName( + context, + AppRoutes.bookAppointmenSchedulesView, + arguments: ScreenArgumentsForAppointmentDetailPage(routeFlag: 2, appointmentId: appointmentListModel.id ?? 0), + ); // 2 For Rescheduling an Appointment + + notifyListeners(); + } + + Future onRescheduleAppointmentConfirmPressed({required BuildContext context, required int appointmentId, required int selectedSlotId}) async { + Utils.showLoading(context); + try { + GenericRespModel genericRespModel = await scheduleRepo.cancelOrRescheduleServiceAppointment( + serviceAppointmentID: appointmentId, + serviceSlotID: selectedSlotId, + appointmentScheduleAction: 1, // 1 for Reschedule and 2 for Cancel + ); + + if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { + Utils.hideLoading(context); + Utils.showToast("${genericRespModel.message.toString()}"); + return; + } + if (genericRespModel.data == 1) { + context.read().onNavbarTapped(1); + applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.cancelled); + Utils.showToast("${genericRespModel.message.toString()}"); + getMyAppointments(); + Utils.hideLoading(context); + navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); + } + } catch (e) { + Utils.showToast("${e.toString()}"); + } + } +} diff --git a/lib/view_models/dashboard_view_model_customer.dart b/lib/view_models/dashboard_view_model_customer.dart new file mode 100644 index 0000000..d8ab0d4 --- /dev/null +++ b/lib/view_models/dashboard_view_model_customer.dart @@ -0,0 +1,55 @@ +import 'dart:io'; +import 'package:mc_common_app/classes/app_state.dart'; +import 'package:mc_common_app/generated/locale_keys.g.dart'; +import 'package:mc_common_app/models/user/image_response.dart'; +import 'package:mc_common_app/repositories/user_repo.dart'; +import 'package:mc_common_app/services/common_services.dart'; +import 'package:mc_common_app/utils/utils.dart'; +import 'package:mc_common_app/view_models/base_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class DashboardVmCustomer extends BaseVM { + final CommonAppServices commonServices; + final UserRepo userRepo; + + DashboardVmCustomer({required this.commonServices, required this.userRepo}); + + String pickedImage = ""; + + int selectedNavbarBarIndex = 2; + + void onNavbarTapped(int index) { + selectedNavbarBarIndex = index; + notifyListeners(); + } + + void pickImageFromPhone(BuildContext context, int sourceFlag) async { + final File? pickedImageFile = await commonServices.pickImageFromPhone(sourceFlag); + if (pickedImageFile == null) { + return; + } + int sizeInBytes = pickedImageFile.lengthSync(); + if (sizeInBytes > 1000) { + Utils.showToast(LocaleKeys.fileLarger.tr()); + return; + } else { + String image64 = Utils.convertFileToBase64(pickedImageFile); + + Utils.showLoading(context); + ImageResponse response = await userRepo.updateUserImage(image64); + Utils.hideLoading(context); + Navigator.pop(context); + if (response.messageStatus == 1) { + Utils.showToast(LocaleKeys.imageUploaded.tr()); + AppState().getUser.data!.userInfo!.userImageUrl = response.data; + } else { + Utils.showToast(response.message ?? ""); + } + } + } + + Future updateUserImage(String image) async { + return await userRepo.updateUserImage(image); + } +} diff --git a/lib/view_models/requests_view_model.dart b/lib/view_models/requests_view_model.dart index 8b1177c..32bce7b 100644 --- a/lib/view_models/requests_view_model.dart +++ b/lib/view_models/requests_view_model.dart @@ -1,272 +1,311 @@ -// import 'dart:io'; -// -// import 'package:mc_common_app/classes/app_state.dart'; -// import 'package:mc_common_app/models/advertisment_models/vehicle_details_models.dart'; -// import 'package:mc_common_app/models/enums_model.dart'; -// import 'package:mc_common_app/models/generic_resp_model.dart'; -// import 'package:mc_common_app/models/requests/request_model.dart'; -// import 'package:mc_common_app/models/widgets_models.dart'; -// import 'package:mc_common_app/repositories/common_repo.dart'; -// import 'package:mc_common_app/services/common_services.dart'; -// import 'package:mc_common_app/utils/utils.dart'; -// import 'package:mc_common_app/view_models/base_view_model.dart'; -// -// import '../repositories/request_repo.dart'; -// -// class RequestsVM extends BaseVM { -// final CommonAppServices commonServices; -// final CommonRepo commonRepo; -// final RequestRepo requestRepo; -// -// RequestsVM({required this.commonServices, required this.commonRepo, required this.requestRepo}); -// -// List requestsFilterOptions = []; -// -// populateRequestsFilterList() { -// requestsFilterOptions.clear(); -// requestsFilterOptions = [ -// FilterListModel(title: "Cars", isSelected: true, id: 1), -// FilterListModel(title: "Spare Parts", isSelected: false, id: 2), -// ]; -// notifyListeners(); -// } -// -// applyFilterOnRequestsVM({required int index}) { -// if (requestsFilterOptions.isEmpty) return; -// for (var value in requestsFilterOptions) { -// value.isSelected = false; -// } -// requestsFilterOptions[index].isSelected = true; -// notifyListeners(); -// } -// -// List pickedVehicleImages = []; -// String vehicleImageError = ""; -// -// void removeImageFromList(String filePath) { -// int index = pickedVehicleImages.indexWhere((element) => element.path == filePath); -// if (index == -1) { -// return; -// } -// pickedVehicleImages.removeAt(index); -// notifyListeners(); -// } -// -// void pickMultipleImages() async { -// List Images = await commonServices.pickMultipleImages(); -// pickedVehicleImages.addAll(Images); -// if (pickedVehicleImages.isNotEmpty) vehicleImageError = ""; -// notifyListeners(); -// } -// -// bool isFetchingRequestType = false; -// bool isFetchingVehicleType = true; -// bool isFetchingVehicleDetail = false; -// List requestTypes = []; -// List vehicleTypes = []; -// VehicleDetailsModel? vehicleDetails; -// List vehicleBrands = []; -// List vehicleModels = []; -// List vehicleModelYears = []; -// List vehicleCountries = []; -// List vehicleCities = []; -// -// SelectionModel requestTypeId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// -// getRequestTypes() async { -// requestTypeId.selectedId = -1; -// isFetchingRequestType = true; -// requestTypes = await commonRepo.getEnumTypeValues(enumTypeID: 16); //TODO: 16 is to get Request types -// isFetchingRequestType = false; -// notifyListeners(); -// } -// -// void updateSelectionRequestTypeId(SelectionModel id) async { -// requestTypeId = id; -// getVehicleTypes(); -// notifyListeners(); -// } -// -// SelectionModel vehicleTypeId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// -// Future getVehicleTypes() async { -// reset(); -// isFetchingVehicleType = true; -// vehicleTypes = await commonRepo.getVehicleTypes(); -// isFetchingVehicleType = false; -// notifyListeners(); -// } -// -// reset() { -// vehicleTypeId.selectedId = -1; -// vehicleBrandId.selectedId = -1; -// vehicleModelId.selectedId = -1; -// vehicleModelYearId.selectedId = -1; -// vehicleCountryId.selectedId = -1; -// vehicleCityId.selectedId = -1; -// } -// -// void updateSelectionVehicleTypeId(SelectionModel id) async { -// vehicleTypeId = id; -// getVehicleBrandsByVehicleTypeId(); -// notifyListeners(); -// } -// -// Future getVehicleBrandsByVehicleTypeId() async { -// // if (vehicleBrandId.selectedId == -1) { -// // return; -// // } -// isFetchingVehicleDetail = true; -// notifyListeners(); -// vehicleDetails = await commonRepo.getVehicleDetails(vehicleTypeId: vehicleTypeId.selectedId); -// -// if (vehicleDetails != null) { -// vehicleBrands = vehicleDetails!.vehicleBrands!; -// vehicleModels = vehicleDetails!.vehicleModels!; -// vehicleModelYears = vehicleDetails!.vehicleModelYears!; -// vehicleCountries = vehicleDetails!.vehicleCountries!; -// } -// isFetchingVehicleDetail = false; -// notifyListeners(); -// } -// -// SelectionModel vehicleBrandId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// -// void updateSelectionVehicleBrandId(SelectionModel id) { -// vehicleBrandId = id; -// vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// vehicleModelYearId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// notifyListeners(); -// } -// -// SelectionModel vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// -// void updateSelectionVehicleModelId(SelectionModel id) { -// vehicleModelId = id; -// vehicleModelYearId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// notifyListeners(); -// } -// -// SelectionModel vehicleModelYearId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// -// void updateSelectionVehicleModelYearId(SelectionModel id) { -// vehicleModelYearId = id; -// notifyListeners(); -// } -// -// bool isShippingDeliveryEnabled = false; -// -// void updateShippingDeliverEnabled(bool v) { -// isShippingDeliveryEnabled = v; -// notifyListeners(); -// } -// -// bool isCountryFetching = false; -// SelectionModel vehicleCountryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// -// void updateSelectionVehicleCountryId(SelectionModel id) async { -// vehicleCountryId = id; -// isCountryFetching = true; -// notifyListeners(); -// vehicleCities = await commonRepo.getVehicleCities(countryId: vehicleCountryId.selectedId); -// isCountryFetching = false; -// notifyListeners(); -// } -// -// SelectionModel vehicleCityId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); -// -// void updateSelectionVehicleCityId(SelectionModel id) { -// vehicleCityId = id; -// notifyListeners(); -// } -// -// //Request Management -// String price = "", description = ""; -// -// updatePrice(String v) { -// price = v; -// } -// -// updateDescription(String v) { -// description = v; -// } -// -// Future createRequest() async { -// if (validate()) { -// Map m = { -// "customerID": AppState().getUser.data!.userInfo!.customerId ?? 0, -// "requestType": requestTypeId.selectedId, -// "vehicleTypeID": vehicleTypeId.selectedId, -// "brand": vehicleBrandId.selectedOption, -// "model": vehicleModelId.selectedOption, -// "year": vehicleModelYearId.selectedOption, -// "isNew": true, -// "countryID": vehicleCountryId.selectedId, -// "cityID": vehicleCityId.selectedId, -// "price": price, -// "description": description, -// "isSpecialServiceNeeded": false, -// "requestImages": [] -// }; -// GenericRespModel respModel = await requestRepo.createRequest(m); -// return respModel; -// } else { -// return null; -// } -// } -// -// bool validate() { -// bool isValid = true; -// if (requestTypeId.selectedId == -1) { -// Utils.showToast("Please select valid Request Type"); -// isValid = false; -// } else if (vehicleTypeId.selectedId == -1) { -// Utils.showToast("Please select valid Vehicle Type"); -// isValid = false; -// } else if (vehicleBrandId.selectedId == -1) { -// Utils.showToast("Please select valid Brand"); -// isValid = false; -// } else if (vehicleModelId.selectedId == -1) { -// Utils.showToast("Please select valid Model"); -// isValid = false; -// } else if (vehicleModelYearId.selectedId == -1) { -// Utils.showToast("Please select valid Year"); -// isValid = false; -// } else if (vehicleCountryId.selectedId == -1) { -// Utils.showToast("Please select valid Country"); -// isValid = false; -// } else if (vehicleCityId.selectedId == -1) { -// Utils.showToast("Please select valid City"); -// isValid = false; -// } else if (price.isEmpty) { -// Utils.showToast("Please add valid Price"); -// isValid = false; -// } else if (description.isEmpty) { -// Utils.showToast("Please add valid Description"); -// isValid = false; -// } -// return isValid; -// } -// -// bool isRequestLoading = true; -// List requests = []; -// -// getRequests() async { -// isRequestLoading = true; -// notifyListeners(); -// -// int selectedRequestType; -// // Find the FilterListModel with isSelected equal to true -// -// requests = await requestRepo.getRequests( -// { -// "customerID": AppState().getUser.data!.userInfo!.customerId, -// "pageSize": 100, -// "pageIndex": 0, -// "requestType": requestsFilterOptions.firstWhere((element) => element.isSelected).id, -// }, -// ); -// isRequestLoading = false; -// notifyListeners(); -// } -// } +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:mc_common_app/classes/app_state.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/advertisment_models/vehicle_details_models.dart'; +import 'package:mc_common_app/models/enums_model.dart'; +import 'package:mc_common_app/models/generic_resp_model.dart'; +import 'package:mc_common_app/models/requests/offers_model.dart'; +import 'package:mc_common_app/models/requests/request_model.dart'; +import 'package:mc_common_app/models/widgets_models.dart'; +import 'package:mc_common_app/repositories/common_repo.dart'; +import 'package:mc_common_app/repositories/request_repo.dart'; +import 'package:mc_common_app/services/common_services.dart'; +import 'package:mc_common_app/utils/enums.dart'; +import 'package:mc_common_app/utils/utils.dart'; +import 'package:mc_common_app/view_models/base_view_model.dart'; + +class RequestsVM extends BaseVM { + final CommonAppServices commonServices; + final CommonRepo commonRepo; + final RequestRepo requestRepo; + + RequestsVM({required this.commonServices, required this.commonRepo, required this.requestRepo}); + + List myRequests = []; + List myFilteredRequests = []; + + List requestsTypeFilterOptions = []; + + List myRequestsTypeEnum = []; + + populateRequestsFilterList() async { + requestsTypeFilterOptions.clear(); + + myRequestsTypeEnum = await commonRepo.getEnumTypeValues(enumTypeID: 16); //TODO: 13 is to get Requests Filter Enums + + for (int i = 0; i < myRequestsTypeEnum.length; i++) { + requestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, isSelected: false, id: myRequestsTypeEnum[i].enumValue)); + } + + notifyListeners(); + } + + Future getMyRequests({bool isNeedToRebuild = false}) async { + if (isNeedToRebuild) setState(ViewState.busy); + + myRequests = await requestRepo.getRequests( + { + "customerID": AppState().getUser.data!.userInfo!.customerId, + "pageSize": 100, + "pageIndex": 0, + "requestType": 0, + }, + ); + applyFilterOnRequestsVM(requestsTypeEnum: RequestsTypeEnum.specialCarRequest); + setState(ViewState.idle); + notifyListeners(); + } + + applyFilterOnRequestsVM({required RequestsTypeEnum requestsTypeEnum}) { + if (requestsTypeFilterOptions.isEmpty) return; + for (var value in requestsTypeFilterOptions) { + value.isSelected = false; + } + requestsTypeFilterOptions[requestsTypeEnum.getIdFromRequestTypeStatusEnum() - 1].isSelected = true; // -1 to match with the index + myFilteredRequests = myRequests.where((element) => element.requestType == requestsTypeEnum.getIdFromRequestTypeStatusEnum()).toList(); + + notifyListeners(); + } + + List pickedVehicleImages = []; + String vehicleImageError = ""; + + void removeImageFromList(String filePath) { + int index = pickedVehicleImages.indexWhere((element) => element.path == filePath); + if (index == -1) { + return; + } + pickedVehicleImages.removeAt(index); + notifyListeners(); + } + + void pickMultipleImages() async { + List Images = await commonServices.pickMultipleImages(); + pickedVehicleImages.addAll(Images); + if (pickedVehicleImages.isNotEmpty) vehicleImageError = ""; + notifyListeners(); + } + + bool isFetchingRequestType = false; + bool isFetchingVehicleType = true; + bool isFetchingVehicleDetail = false; + List vehicleTypes = []; + VehicleDetailsModel? vehicleDetails; + List vehicleBrands = []; + List vehicleModels = []; + List vehicleModelYears = []; + List vehicleCountries = []; + List vehicleCities = []; + + SelectionModel requestTypeId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updateSelectionRequestTypeId(SelectionModel id) async { + requestTypeId = id; + getVehicleTypes(); + notifyListeners(); + } + + SelectionModel vehicleTypeId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + Future getVehicleTypes() async { + resetRequestCreationForm(); + isFetchingVehicleType = true; + vehicleTypes = await commonRepo.getVehicleTypes(); + isFetchingVehicleType = false; + notifyListeners(); + } + + resetRequestCreationForm() { + vehicleTypeId.selectedId = -1; + vehicleBrandId.selectedId = -1; + vehicleModelId.selectedId = -1; + vehicleModelYearId.selectedId = -1; + vehicleCountryId.selectedId = -1; + vehicleCityId.selectedId = -1; + } + + void updateSelectionVehicleTypeId(SelectionModel id) async { + vehicleTypeId = id; + getVehicleBrandsByVehicleTypeId(); + notifyListeners(); + } + + Future getVehicleBrandsByVehicleTypeId() async { + // if (vehicleBrandId.selectedId == -1) { + // return; + // } + isFetchingVehicleDetail = true; + notifyListeners(); + vehicleDetails = await commonRepo.getVehicleDetails(vehicleTypeId: vehicleTypeId.selectedId); + + if (vehicleDetails != null) { + vehicleBrands = vehicleDetails!.vehicleBrands!; + vehicleModels = vehicleDetails!.vehicleModels!; + vehicleModelYears = vehicleDetails!.vehicleModelYears!; + vehicleCountries = vehicleDetails!.vehicleCountries!; + } + isFetchingVehicleDetail = false; + notifyListeners(); + } + + SelectionModel vehicleBrandId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updateSelectionVehicleBrandId(SelectionModel id) { + vehicleBrandId = id; + vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + vehicleModelYearId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + notifyListeners(); + } + + SelectionModel vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updateSelectionVehicleModelId(SelectionModel id) { + vehicleModelId = id; + vehicleModelYearId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + notifyListeners(); + } + + SelectionModel vehicleModelYearId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updateSelectionVehicleModelYearId(SelectionModel id) { + vehicleModelYearId = id; + notifyListeners(); + } + + bool isShippingDeliveryEnabled = false; + + void updateShippingDeliverEnabled(bool v) { + isShippingDeliveryEnabled = v; + notifyListeners(); + } + + bool isCountryFetching = false; + SelectionModel vehicleCountryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updateSelectionVehicleCountryId(SelectionModel id) async { + vehicleCountryId = id; + isCountryFetching = true; + notifyListeners(); + vehicleCities = await commonRepo.getVehicleCities(countryId: vehicleCountryId.selectedId); + isCountryFetching = false; + notifyListeners(); + } + + SelectionModel vehicleCityId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + + void updateSelectionVehicleCityId(SelectionModel id) { + vehicleCityId = id; + notifyListeners(); + } + + //Request Management + String price = "", description = ""; + + updatePrice(String v) { + price = v; + } + + updateDescription(String v) { + description = v; + } + + Future convertFileToRequestPostingImages({required File file}) async { + List imageBytes = await file.readAsBytes(); + String image = base64Encode(imageBytes); + String fileName = file.path.split('/').last; + VehiclePostingImages vehiclePostingImages = VehiclePostingImages( + imageName: fileName, + imageStr: image, + imageUrl: file.path, + ); + return vehiclePostingImages; + } + + Future onCreateRequestTapped(BuildContext context) async { + if (validateCreateRequestForm()) { + Utils.showLoading(context); + List vehicleImages = []; + pickedVehicleImages.forEach((element) async { + vehicleImages.add(await convertFileToRequestPostingImages(file: element)); + }); + + Map body = { + "customerID": AppState().getUser.data!.userInfo!.customerId ?? 0, + "requestType": requestTypeId.selectedId, + "vehicleTypeID": vehicleTypeId.selectedId, + "brand": vehicleBrandId.selectedOption, + "model": vehicleModelId.selectedOption, + "year": vehicleModelYearId.selectedOption, + "isNew": true, + "countryID": vehicleCountryId.selectedId, + "cityID": vehicleCityId.selectedId, + "price": price, + "description": description, + "isSpecialServiceNeeded": false, + "requestImages": vehicleImages, + }; + try { + GenericRespModel respModel = await requestRepo.createRequest(body); + Utils.hideLoading(context); + if (respModel.messageStatus == 1) { + Utils.showToast("Request Successfully Created"); + Navigator.pop(context); + await getMyRequests(); + } else { + Utils.showToast(respModel.message.toString()); + } + } catch (e, s) { + Utils.hideLoading(context); + print(s); + } + } + } + + bool validateCreateRequestForm() { + bool isValid = true; + if (requestTypeId.selectedId == -1) { + Utils.showToast("Please select valid Request Type"); + isValid = false; + } else if (vehicleTypeId.selectedId == -1) { + Utils.showToast("Please select valid Vehicle Type"); + isValid = false; + } else if (vehicleBrandId.selectedId == -1) { + Utils.showToast("Please select valid Brand"); + isValid = false; + } else if (vehicleModelId.selectedId == -1) { + Utils.showToast("Please select valid Model"); + isValid = false; + } else if (vehicleModelYearId.selectedId == -1) { + Utils.showToast("Please select valid Year"); + isValid = false; + } else if (vehicleCountryId.selectedId == -1) { + Utils.showToast("Please select valid Country"); + isValid = false; + } else if (vehicleCityId.selectedId == -1) { + Utils.showToast("Please select valid City"); + isValid = false; + } else if (price.isEmpty) { + Utils.showToast("Please add valid Price"); + isValid = false; + } else if (description.isEmpty) { + Utils.showToast("Please add valid Description"); + isValid = false; + } + return isValid; + } + + Future> getOffersByRequest({required int requestId, required BuildContext context}) async { + try { + Utils.showLoading(context); + List respModel = await requestRepo.getOffersByRequest(requestId: requestId); + Utils.hideLoading(context); + return respModel; + } catch (e) { + Utils.showToast(e.toString()); + Utils.hideLoading(context); + return []; + } + } +} diff --git a/lib/views/advertisement/ads_detail_view.dart b/lib/views/advertisement/ads_detail_view.dart index cb40c61..dc4145b 100644 --- a/lib/views/advertisement/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view.dart @@ -6,6 +6,7 @@ import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart'; +import 'package:mc_common_app/models/advertisment_models/special_service_model.dart'; import 'package:mc_common_app/models/widgets_models.dart'; import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/utils/dialogs_and_bottomsheets.dart'; @@ -20,8 +21,10 @@ import 'package:mc_common_app/widgets/bottom_sheet.dart'; import 'package:mc_common_app/widgets/button/show_fill_button.dart'; import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; import 'package:mc_common_app/widgets/common_widgets/info_bottom_sheet.dart'; +import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:provider/provider.dart'; +import 'package:sizer/sizer.dart'; class AdsDetailView extends StatelessWidget { final AdDetailsModel adDetails; @@ -64,7 +67,7 @@ class AdsDetailView extends StatelessWidget { @override Widget build(BuildContext context) { print("adId: ${adDetails.id}"); - print("statusID: ${adDetails.statusID}"); + print("statusID: ${adDetails.specialservice!.first.name}"); context.read().updateCurrentAdId(id: adDetails.id!); return Scaffold( appBar: CustomAppBar( @@ -260,20 +263,6 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { ) ], ), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // "Tax".toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), - // Row( - // crossAxisAlignment: CrossAxisAlignment.end, - // children: [ - // "${adDetailsModel.taxPrice}".toText(fontSize: 16, isBold: true), - // 2.width, - // "SAR".toText(color: MyColors.lightTextColor, fontSize: 10, isBold: true).paddingOnly(bottom: 0), - // ], - // ) - // ], - // ), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, @@ -505,6 +494,85 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { const BuildAdDetailsActionButtonForMyAds({Key? key, required this.adDetailsModel}) : super(key: key); + void onBookPhotographyServiceClicked(BuildContext context) async { + AdVM adVM = context.read(); + if (adVM.photoOfficeSelectedId.selectedId == -1) { + adVM.getPhotographyServiceScheduleListByOffices(latitude: 46.703430, longitude: 24.625720); + } + + return showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: true, + builder: (BuildContext context) { + return InfoBottomSheet( + title: "Set Date and Time".toText(fontSize: 28, isBold: true, letterSpacing: -1.44, height: 1.2), + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 25.height, + Builder( + builder: (context) { + List vehicleCitiesDrop = []; + for (var element in adVM.photoSSSchedulesByOffices) { + vehicleCitiesDrop.add(DropValue(element.photoOfficeID?.toInt() ?? 0, element.photoOfficeName ?? "", "")); + } + return DropdownField( + (DropValue value) => adVM.updatePhotoOfficeSelectedId(SelectionModel(selectedId: value.id, selectedOption: value.value)), + list: vehicleCitiesDrop, + dropdownValue: adVM.photoOfficeSelectedId.selectedId != -1 ? DropValue(adVM.photoOfficeSelectedId.selectedId, adVM.photoOfficeSelectedId.selectedOption, "") : null, + hint: "Select Office", + errorValue: adVM.photoOfficeSelectedId.errorValue, + ); + }, + ), + if (adVM.photoOfficeSelectedId.selectedId != -1) ...[ + 9.height, + // CustomCalenderWidget(customTimeDateSlotList: [], scheduleIndex: scheduleIndex), + // if (appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex != null) ...[ + // 5.height, + // Row( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // ("Available Slots").toText(fontSize: 14, isBold: true), + // ], + // ), + // 5.height, + // SizedBox( + // width: double.infinity, + // child: BuildTimeSlots( + // timeSlots: appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex!] + // .availableSlots ?? + // [], + // onPressed: (slotIndex) { + // appointmentsVM.updateSelectedAppointmentSlotByDate(scheduleIndex: scheduleIndex, slotIndex: slotIndex); + // }, + // ), + // ), + // ], + 5.height, + Row( + children: [ + Expanded( + child: ShowFillButton( + maxHeight: 55, + title: "Book and Pay", + fontSize: 15, + onPressed: () { + Navigator.pop(context); + }, + ), + ), + ], + ), + ], + 19.height, + ], + )); + }, + ); + } + void reserveAdPriceBreakDownClicked(BuildContext context) { showModalBottomSheet( context: context, @@ -671,15 +739,43 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { ); } - Widget pendingForPaymentAction(BuildContext context, {required int adID}) { + Widget pendingForPaymentAction(BuildContext context, {required AdDetailsModel ad}) { + SpecialServiceModelForAds? photoSpecialServiceModel; + for (var element in ad.specialservice!) { + if (element.specialServiceID == 1) { + photoSpecialServiceModel = element; + } + } return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ + if (photoSpecialServiceModel != null) ...[ + Row( + children: [ + Expanded( + child: ShowFillButton( + isFilled: false, + borderColor: MyColors.darkPrimaryColor, + maxHeight: 55, + title: "Book ${photoSpecialServiceModel.name}", + txtColor: MyColors.darkPrimaryColor, + onPressed: () { + onBookPhotographyServiceClicked(context); + }, + ), + ), + ], + ), + 8.height, + ], Row( children: [ Expanded( child: ShowFillButton( maxHeight: 55, + backgroundColor: MyColors.grey98Color.withOpacity(0.3), + txtColor: MyColors.lightTextColor, + isBold: false, title: "Pay Now", onPressed: () { navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.ads); @@ -869,7 +965,7 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { Widget build(BuildContext context) { switch (adDetailsModel.adPostStatus!) { case AdPostStatus.pendingForPayment: - return pendingForPaymentAction(context, adID: adDetailsModel.id!); + return pendingForPaymentAction(context, ad: adDetailsModel); case AdPostStatus.active: return markAsSoldAction(context); case AdPostStatus.reserved: diff --git a/lib/views/appointments/appointment_detail_view.dart b/lib/views/appointments/appointment_detail_view.dart new file mode 100644 index 0000000..4eca313 --- /dev/null +++ b/lib/views/appointments/appointment_detail_view.dart @@ -0,0 +1,245 @@ +import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/utils/dialogs_and_bottomsheets.dart'; +import 'package:mc_common_app/utils/enums.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; +import 'package:mc_common_app/widgets/common_widgets/card_button_with_icon.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; + +class AppointmentDetailView extends StatelessWidget { + final AppointmentListModel appointmentListModel; + + AppointmentDetailView({Key? key, required this.appointmentListModel}) : super(key: key); + + Widget getBaseActionButtonWidget({required Color color, required String text, Color textColor = MyColors.white, required Function() onPressed}) { + return Expanded( + child: ShowFillButton( + maxHeight: 55, + title: text, + onPressed: onPressed, + backgroundColor: color, + txtColor: textColor, + fontSize: 18, + ), + ); + } + + Widget buildBottomActionButton({required AppointmentStatusEnum appointmentStatusEnum, required BuildContext context}) { + switch (appointmentStatusEnum) { + case AppointmentStatusEnum.booked: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.redColor, onPressed: () => appointmentCancelConfirmationSheet(context), text: "Cancel"), + 12.width, + getBaseActionButtonWidget( + color: MyColors.greenColor, + onPressed: () { + context.read().onConfirmAppointmentPressed(context: context, appointmentId: appointmentListModel.id); + }, + text: "Confirm"), + ], + ), + ); + case AppointmentStatusEnum.confirmed: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.redColor, onPressed: () => appointmentCancelConfirmationSheet(context), text: "Cancel"), + ], + ), + ); + case AppointmentStatusEnum.arrived: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.grey98Color.withOpacity(0.3), textColor: MyColors.lightTextColor, onPressed: () {}, text: "In Progress"), + ], + ), + ); + case AppointmentStatusEnum.cancelled: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.grey98Color.withOpacity(0.3), textColor: MyColors.lightTextColor, onPressed: () {}, text: "Cancelled"), + ], + ), + ); + case AppointmentStatusEnum.allAppointments: + return SizedBox(); + case AppointmentStatusEnum.rescheduled: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.redColor, onPressed: () => appointmentCancelConfirmationSheet(context), text: "Cancel"), + 12.width, + getBaseActionButtonWidget( + color: MyColors.greenColor, + onPressed: () { + context.read().onConfirmAppointmentPressed(context: context, appointmentId: appointmentListModel.id); + }, + text: "Confirm"), + ], + ), + ); + } + } + + void appointmentCancelConfirmationSheet(BuildContext context) { + final appointmentsVm = context.read(); + return actionConfirmationBottomSheet( + context: context, + title: "Do you want to cancel this appointment?".toText(fontSize: 28, isBold: true, letterSpacing: -1.44), + subtitle: "Your appointment will be cancelled and you cannot undo this action.", + actionButtonYes: Expanded( + child: ShowFillButton( + maxHeight: 55, + title: "Yes", + fontSize: 15, + onPressed: () { + Navigator.pop(context); + appointmentsVm.onCancelAppointmentPressed(context: context, appointmentListModel: appointmentListModel); + }, + ), + ), + actionButtonNo: Expanded( + child: ShowFillButton( + maxHeight: 55, + isFilled: false, + borderColor: MyColors.darkPrimaryColor, + title: "No", + txtColor: MyColors.darkPrimaryColor, + fontSize: 15, + onPressed: () { + Navigator.pop(context); + }, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + AppointmentsVM appointmentsVM = context.read(); + return Scaffold( + appBar: CustomAppBar( + title: "Appointment", + profileImageUrl: MyAssets.bnCar, + isRemoveBackButton: false, + isDrawerEnabled: false, + ), + body: Container( + padding: const EdgeInsets.only(bottom: 10, left: 21, right: 21), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + appointmentListModel.providerName!.toText(fontSize: 16, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MyAssets.miniClockDark.buildSvg( + height: 12, + width: 12, + fit: BoxFit.fill, + ), + 5.width, + "${appointmentListModel.duration ?? ""} ${appointmentListModel.appointmentDate!.toFormattedDateWithoutTime()}".toText(fontSize: 12, isBold: true, color: MyColors.lightTextColor), + ], + ), + 13.height, + if (appointmentListModel.appointmentServicesList != null && appointmentListModel.appointmentServicesList!.isNotEmpty) ...[ + Column( + children: List.generate(appointmentListModel.appointmentServicesList!.length, (index) { + ServiceModel service = appointmentListModel.appointmentServicesList![index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // MyAssets.maintenanceIcon.buildSvg( + // height: 10, + // width: 10, + // fit: BoxFit.fill, + // ), + // 10.width, + "${index + 1}. ${service.providerServiceDescription}".toText(fontSize: 14, isBold: true), + ], + ), + if (service.serviceItems != null && service.serviceItems!.isNotEmpty) ...[ + Column( + children: List.generate( + service.serviceItems!.length, + (index) => "${service.serviceItems![index].name}".toText( + textAlign: TextAlign.start, + fontSize: 12, + color: MyColors.lightTextColor, + ), + ), + ).paddingOnly(left: 15), + ], + 5.height, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ((service.currentTotalServicePrice).toString()).toText(fontSize: 25, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), + Icon( + Icons.arrow_drop_down, + size: 30, + ) + ], + ).onPress(() => appointmentsVM.priceBreakDownClicked(context, service)), + ], + ); + }), + ), + ], + 15.height, + Row( + children: [ + CardButtonWithIcon( + title: "Reschedule Appointment", + onCardTapped: () { + context.read().onRescheduleAppointmentPressed(context: context, appointmentListModel: appointmentListModel); + }, + icon: MyAssets.scheduleAppointmentIcon.buildSvg(), + ), + if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.booked) ...[ + 10.width, + CardButtonWithIcon( + title: "Pay for Appointment", + onCardTapped: () { + context.read().onConfirmAppointmentPressed(context: context, appointmentId: appointmentListModel.id); + }, + icon: MyAssets.creditCardIcon.buildSvg(), + ), + ], + ], + ), + 15.height, + ], + ).toWhiteContainer(width: double.infinity, allPading: 12), + buildBottomActionButton(appointmentStatusEnum: appointmentListModel.appointmentStatusEnum!, context: context), + ], + ), + ), + ); + } +} diff --git a/lib/views/appointments/book_appointment_schedules_view.dart b/lib/views/appointments/book_appointment_schedules_view.dart new file mode 100644 index 0000000..da6204d --- /dev/null +++ b/lib/views/appointments/book_appointment_schedules_view.dart @@ -0,0 +1,159 @@ + +import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/service_schedule_model.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; +import 'package:mc_common_app/views/appointments/widgets/custom_calender_widget.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; +import 'package:mc_common_app/widgets/common_widgets/time_slots.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; + +class ScreenArgumentsForAppointmentDetailPage { + final int routeFlag; // 1 = coming from create appointment || 2 = coming from reschedule appointment + final int appointmentId; // 1 = coming from create appointment || 2 = coming from reschedule appointment + + ScreenArgumentsForAppointmentDetailPage({required this.routeFlag, required this.appointmentId}); +} + +class BookAppointmentSchedulesView extends StatelessWidget { + final ScreenArgumentsForAppointmentDetailPage screenArgumentsForAppointmentDetailPage; + + BookAppointmentSchedulesView({Key? key, required this.screenArgumentsForAppointmentDetailPage}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: "Book Appointment", + isRemoveBackButton: false, + isDrawerEnabled: false, + onBackButtonTapped: () => Navigator.pop(context), + ), + body: Consumer( + builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 21.height, + ListView.builder( + shrinkWrap: true, + itemCount: appointmentsVM.serviceAppointmentScheduleList.length, + itemBuilder: (BuildContext context, int scheduleIndex) { + ServiceAppointmentScheduleModel scheduleData = appointmentsVM.serviceAppointmentScheduleList[scheduleIndex]; + return ExpansionTile( + tilePadding: EdgeInsets.symmetric(horizontal: 21, vertical: 10), + childrenPadding: EdgeInsets.only(left: 16, bottom: 10, right: 16), + title: Column( + children: [ + Row( + children: [ + Expanded( + child: "Schedule ${scheduleIndex + 1}".toText(fontSize: 20, isBold: true), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Service Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + ("${scheduleData.appointmentType == 2 ? "Home" : "Workshop"}").toText(fontSize: 12, isBold: true).expand(), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 5.height, + ListView.builder( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].servicesListInAppointment!.length, + itemBuilder: (BuildContext context, int serviceIndex) { + ServiceModel selectedService = appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].servicesListInAppointment![serviceIndex]; + return Row( + children: [ + Expanded( + child: ("${serviceIndex + 1}. ${selectedService.providerServiceDescription}").toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor), + ), + ], + ); + }, + ), + ], + ), + ], + ), + children: [ + Column( + children: [ + CustomCalenderWidget(customTimeDateSlotList: scheduleData.customTimeDateSlotList ?? [], scheduleIndex: scheduleIndex), + if (appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex != null) ...[ + 5.height, + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ("Available Slots").toText(fontSize: 14, isBold: true), + ], + ), + 5.height, + SizedBox( + width: double.infinity, + child: BuildTimeSlots( + timeSlots: appointmentsVM.serviceAppointmentScheduleList[scheduleIndex] + .customTimeDateSlotList![appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex!].availableSlots ?? + [], + onPressed: (slotIndex) { + appointmentsVM.updateSelectedAppointmentSlotByDate(scheduleIndex: scheduleIndex, slotIndex: slotIndex); + }, + ), + ), + ], + ], + ), + ], + ).toWhiteContainer(width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10)); + }, + ).expand(), + Row( + children: [ + Expanded( + child: ShowFillButton( + txtColor: MyColors.black, + maxHeight: 55, + title: "Cancel", + onPressed: () => Navigator.pop(context), + backgroundColor: MyColors.greyButtonColor, + ), + ), + 12.width, + Expanded( + child: ShowFillButton( + maxHeight: 55, + title: screenArgumentsForAppointmentDetailPage.routeFlag == 1 ? "Review" : "Confirm", + onPressed: () { + if (screenArgumentsForAppointmentDetailPage.routeFlag == 1) { + appointmentsVM.onReviewButtonPressed(context); + } else { + appointmentsVM.onRescheduleAppointmentConfirmPressed( + context: context, + appointmentId: screenArgumentsForAppointmentDetailPage.appointmentId, + selectedSlotId: appointmentsVM.serviceAppointmentScheduleList.first.selectedCustomTimeDateSlotModel!.date!.slotId, + ); + } + }, + backgroundColor: MyColors.darkPrimaryColor, + ), + ) + ], + ).paddingAll(21) + ], + ); + }, + )); + } +} diff --git a/lib/views/appointments/book_appointment_services_view.dart b/lib/views/appointments/book_appointment_services_view.dart new file mode 100644 index 0000000..cadd2e4 --- /dev/null +++ b/lib/views/appointments/book_appointment_services_view.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/services/item_model.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; +import 'package:mc_common_app/views/advertisement/custom_add_button.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; + +class BookAppointmentServicesView extends StatelessWidget { + BookAppointmentServicesView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: "Book Appointment", + isRemoveBackButton: false, + isDrawerEnabled: false, + onBackButtonTapped: () { + context.read().resetAfterBookingAppointment(); + Navigator.pop(context); + }, + ), + body: Consumer( + builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 21.height, + CustomAddButton( + needsBorder: true, + bgColor: MyColors.white, + onTap: () => appointmentsVM.openTheAddServiceBottomSheet(context, appointmentsVM), + text: "Add Services", + icon: Container( + height: 24, + width: 24, + decoration: const BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), + child: const Icon(Icons.add, color: MyColors.white), + ), + ).horPaddingMain(), + 10.height, + ListView.builder( + shrinkWrap: true, + itemCount: appointmentsVM.servicesInCurrentAppointment.length, + itemBuilder: (BuildContext context, int serviceIndex) { + ServiceModel serviceData = appointmentsVM.servicesInCurrentAppointment[serviceIndex]; + return Column( + children: [ + Row( + children: [ + Expanded( + child: (serviceData.serviceDescription ?? "").toText(fontSize: 15, isBold: true), + ), + IconButton( + onPressed: () => appointmentsVM.removeServiceInCurrentAppointment(serviceIndex), + icon: Icon(Icons.delete_outline, color: MyColors.redColor), + ) + ], + ), + if (true) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Service Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + (serviceData.isHomeSelected ? serviceData.homeLocation : "Workshop").toText(fontSize: 12, isBold: true).expand(), + ], + ), + 5.height, + Column( + children: List.generate(serviceData.serviceItems!.length, (itemIndex) { + ItemData itemData = serviceData.serviceItems![itemIndex]; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${itemData.name}: ".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), + ("${itemData.price}").toText(fontSize: 13, isBold: true).expand(), + ], + ); + }), + ), + 8.height, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ((appointmentsVM.servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice).toString()).toText(fontSize: 32, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), + Icon( + Icons.arrow_drop_down, + size: 30, + ) + ], + ).onPress(() => appointmentsVM.priceBreakDownClicked(context, appointmentsVM.servicesInCurrentAppointment[serviceIndex])), + ], + ], + ).toWhiteContainer(width: double.infinity, allPading: 12, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10)); + }, + ).expand(), + Row( + children: [ + Expanded( + child: ShowFillButton( + txtColor: MyColors.black, + maxHeight: 55, + title: "Cancel", + onPressed: () { + appointmentsVM.servicesInCurrentAppointment.clear(); + appointmentsVM.allSelectedItemsInAppointments.clear(); + Navigator.pop(context); + }, + backgroundColor: MyColors.greyButtonColor, + ), + ), + 12.width, + Expanded( + child: ShowFillButton( + maxHeight: 55, + title: "Next", + onPressed: () { + appointmentsVM.onServicesNextPressed(context); + }, + backgroundColor: MyColors.darkPrimaryColor, + ), + ) + ], + ).paddingAll(21) + ], + ); + }, + )); + } +} diff --git a/lib/views/appointments/book_appointments_item_view.dart b/lib/views/appointments/book_appointments_item_view.dart new file mode 100644 index 0000000..3063fbd --- /dev/null +++ b/lib/views/appointments/book_appointments_item_view.dart @@ -0,0 +1,151 @@ + +import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/services/item_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; +import 'package:mc_common_app/views/appointments/widgets/service_item_with_price_checkbox.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; + +class BookAppointmentsItemView extends StatelessWidget { + const BookAppointmentsItemView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: "Select Services", + isRemoveBackButton: false, + isDrawerEnabled: false, + actions: [MyAssets.searchIcon.buildSvg().paddingOnly(right: 21)], + onBackButtonTapped: () => Navigator.pop(context), + ), + body: Consumer( + builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 40, + width: double.infinity, + color: MyColors.darkTextColor, + alignment: Alignment.centerLeft, + child: "${appointmentsVM.selectedSubServicesCounter} Item(s) Selected".toText(fontSize: 16, color: MyColors.white).horPaddingMain(), + ), + 16.height, + Column( + children: [ + "Few services are not available on home location. Change the location to workshop to full access the services".toText(fontSize: 12, isItalic: true, color: MyColors.lightTextColor), + 8.height, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Change location or service: ".toText(fontSize: 14, isBold: true), + "Edit".toText(fontSize: 14, isBold: true, isUnderLine: true, color: MyColors.adPendingStatusColor), + 5.width, + MyAssets.icEdit.buildSvg(width: 17), + ], + ).onPress(() {}), + 16.height, + Divider(), + ], + ).horPaddingMain(), + appointmentsVM.serviceItemsFromApi.isEmpty + ? Expanded(child: Center(child: "No Items to show.".toText(fontSize: 16, color: MyColors.lightTextColor))) + : ListView.separated( + separatorBuilder: (BuildContext context, int index) => Divider(), + itemCount: appointmentsVM.serviceItemsFromApi.length, + itemBuilder: (BuildContext context, int index) { + ItemData itemData = appointmentsVM.serviceItemsFromApi[index]; + return ServiceItemWithPriceCheckBox( + description: itemData.description ?? "Some description about the sub-services", + title: itemData.name!, + isSelected: itemData.isUpdateOrSelected!, + onSelection: (bool value) { + print("itemId: ${itemData.id}"); + appointmentsVM.onItemUpdateOrSelected(index, !itemData.isUpdateOrSelected!, itemData.id!); + }, + priceWidget: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // TODO: This Price will be decided according to the service selected + itemData.price!.split(".").first.toText(fontSize: 30, isBold: true), + " SAR".toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor).paddingOnly(bottom: 5), + ], + ), + ); + }, + ).expand(), + Column( + children: [ + Divider( + height: 1, + thickness: 0.7, + ), + 8.height, + if (appointmentsVM.selectSubServicesError != "") + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + appointmentsVM.selectSubServicesError.toText(fontSize: 14, color: Colors.red), + ], + ).paddingOnly(right: 10), + 8.height, + Row( + children: [ + Expanded( + child: ShowFillButton( + txtColor: MyColors.black, + maxHeight: 55, + title: "Cancel", + onPressed: () { + appointmentsVM.resetCategorySelectionBottomSheet(); + pop(context); + }, + backgroundColor: MyColors.greyButtonColor, + ), + ), + 12.width, + Expanded( + child: Builder( + builder: (BuildContext context) { + return ShowFillButton( + maxHeight: 55, + title: "Next", + onPressed: () async { + bool resp = appointmentsVM.validateItemsSelection(); + if (resp) { + appointmentsVM.onItemsSelectedInService(); + Navigator.pop(context); + } + // bool resp = appointmentsVM.validateItemsSelection(); + // if (resp) { + // Utils.showLoading(context); + // await appointmentsVM.mergeServiceIntoAvailableSchedules(); + // appointmentsVM.resetCategorySelectionBottomSheet(); + // Utils.hideLoading(context); + // Navigator.of(context).pushReplacementNamed(AppRoutes.bookAppointmenServicesView); + // } + }, + backgroundColor: !appointmentsVM.isServiceSelectionValidated() ? MyColors.lightTextColor.withOpacity(0.6) : MyColors.darkPrimaryColor, + ); + }, + ), + ), + ], + ).horPaddingMain(), + 16.height, + ], + ), + ], + ); + }, + )); + } +} diff --git a/lib/views/appointments/pick_items_view.dart b/lib/views/appointments/pick_items_view.dart new file mode 100644 index 0000000..bd8c1e6 --- /dev/null +++ b/lib/views/appointments/pick_items_view.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; +import 'package:mc_common_app/widgets/common_widgets/provider_details_card.dart'; +import 'package:mc_common_app/widgets/common_widgets/time_slots.dart'; +import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; + +class BookProviderAppView extends StatefulWidget { + const BookProviderAppView({Key? key}) : super(key: key); + + @override + State createState() => _BookProviderAppViewState(); +} + +class _BookProviderAppViewState extends State { + bool isReview = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: "Appointment", + profileImageUrl: MyAssets.bnCar, + isRemoveBackButton: false, + isDrawerEnabled: false, + ), + body: Container( + color: MyColors.backgroundColor, + width: double.infinity, + height: double.infinity, + child: Column( + children: [ + Expanded( + child: ListView( + children: [ + ProviderDetailsCard( + onCardTapped: () {}, + providerImageUrl: MyAssets.bnCar, + providerLocation: "3km", + title: "Al Ahmed Maintenance", + providerRatings: "4.9", + ), + 12.height, + isReview ? ReviewAppointmentSection() : ServicesSelectionSection(), + 10.height, + ], + ), + ), + 10.height, + Padding( + padding: const EdgeInsets.only(bottom: 10, left: 21, right: 21), + child: ShowFillButton( + title: "Book Appointment", + maxWidth: double.infinity, + onPressed: () { + isReview = !isReview; + setState(() {}); + }, + ), + ), + ], + ), + ), + ); + } +} + +class ServicesSelectionSection extends StatelessWidget { + const ServicesSelectionSection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + List dropList = [ + DropValue(0, "Maintenance", ""), + DropValue(1, "Car Wash", ""), + DropValue(2, "Monthly Checkup", ""), + DropValue(3, "Friendly Visit", ""), + DropValue(4, "Muftaa", ""), + ]; + return Container( + padding: const EdgeInsets.only(bottom: 10, left: 21, right: 21), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Select services you want".toText(fontSize: 18, isBold: true), + 8.height, + DropdownField( + (DropValue value) {}, + list: dropList, + hint: "Select service type", + ), + 8.height, + DropdownField( + (DropValue value) {}, + list: dropList, + hint: "Select service type", + ), + 8.height, + DropdownField( + (DropValue value) {}, + list: dropList, + hint: "Select service type", + ), + 22.height, + "Select date and time".toText(fontSize: 18, isBold: true), + 8.height, + DropdownField( + (DropValue value) {}, + list: dropList, + hint: "Select service type", + ), + 22.height, + "Available slots".toText(fontSize: 15, isBold: true), + 8.height, + BuildTimeSlots(onPressed: (index) {}, timeSlots: []), + 22.height, + "Total Amount".toText(fontSize: 18, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + "3000".toText(fontSize: 20, isBold: true), + "SAR".toText(fontSize: 10, isBold: true, color: MyColors.lightTextColor), + ], + ), + 10.height, + ], + ).toWhiteContainer(width: double.infinity, allPading: 12), + ); + } +} + +class ReviewAppointmentSection extends StatelessWidget { + const ReviewAppointmentSection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(bottom: 10, left: 21, right: 21), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Review Appointment".toText(fontSize: 18, isBold: true), + 15.height, + "Services".toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + "Car Engine Check".toText(fontSize: 18, isBold: true), + 13.height, + "Date and Time".toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + "2 Feb, 2023 at 09:00am".toText(fontSize: 18, isBold: true), + 13.height, + "Total Amount".toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + "3000".toText(fontSize: 20, isBold: true), + "SAR".toText(fontSize: 10, isBold: true, color: MyColors.lightTextColor), + ], + ), + 100.height, + ], + ).toWhiteContainer(width: double.infinity, allPading: 12), + ); + } +} diff --git a/lib/views/appointments/review_appointment_view.dart b/lib/views/appointments/review_appointment_view.dart new file mode 100644 index 0000000..94c0bca --- /dev/null +++ b/lib/views/appointments/review_appointment_view.dart @@ -0,0 +1,304 @@ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/generated/locale_keys.g.dart'; +import 'package:mc_common_app/models/service_schedule_model.dart'; +import 'package:mc_common_app/models/services/item_model.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; + +class ReviewAppointment extends StatelessWidget { + const ReviewAppointment({Key? key}) : super(key: key); + + Widget buildBranchInfoCard({required BuildContext context}) { + AppointmentsVM appointmentsVM = context.read(); + return Padding( + padding: const EdgeInsets.only( + bottom: 10, + left: 21, + right: 21, + ), + child: Row( + children: [ + Image.asset( + MyAssets.bnCar, + width: 80, + height: 60, + fit: BoxFit.cover, + ), + 12.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + (appointmentsVM.selectedBranchModel!.branchName ?? "").toText(fontSize: 16, isBold: true), + Row( + children: [ + LocaleKeys.location.tr().toText(color: MyColors.lightTextColor, fontSize: 12), + 2.width, + ": ${appointmentsVM.selectedBranchModel!.branchDescription ?? ""}".toText(fontSize: 12), + ], + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "4.9".toText( + isUnderLine: true, + isBold: true, + fontSize: 12, + ), + 2.width, + MyAssets.starIcon.buildSvg(width: 12), + ], + ), + ], + ), + ], + ), + ), + ], + ).onPress(() {}).toWhiteContainer(width: double.infinity, allPading: 12)); + } + + Widget buildServicesInfoCard({required BuildContext context}) { + AppointmentsVM appointmentsVM = context.read(); + return ListView.builder( + shrinkWrap: true, + itemCount: appointmentsVM.serviceAppointmentScheduleList.length, + itemBuilder: (BuildContext context, int scheduleIndex) { + ServiceAppointmentScheduleModel scheduleData = appointmentsVM.serviceAppointmentScheduleList[scheduleIndex]; + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: "Services".toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: scheduleData.servicesListInAppointment!.length, + itemBuilder: (BuildContext context, int serviceIndex) { + String selectedTimeSlot = ""; + if (scheduleData.selectedCustomTimeDateSlotModel!.availableSlots != null) { + selectedTimeSlot = scheduleData.selectedCustomTimeDateSlotModel!.availableSlots!.firstWhere((element) => element.isSelected).slot; + } + return Column( + children: [ + if (scheduleData.servicesListInAppointment!.isNotEmpty) ...[ + Column( + children: List.generate(scheduleData.servicesListInAppointment!.length, (serviceIndex) { + ServiceModel serviceData = scheduleData.servicesListInAppointment![serviceIndex]; + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${serviceData.providerServiceDescription}".toText(fontSize: 16, isBold: true), + ], + ), + Column( + children: List.generate(serviceData.serviceItems!.length, (itemIndex) { + ItemData itemData = serviceData.serviceItems![itemIndex]; + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "${itemData.name}: ${itemData.price} SAR".toText(fontSize: 13, isBold: true, color: MyColors.lightTextColor), + ], + ), + ], + ); + }), + ), + ], + ); + }), + ).paddingOnly(bottom: 10), + ], + 5.height, + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Time & Location".toText(fontSize: 16, isBold: true), + 3.height, + Row(children: [ + "Date & Time: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "${scheduleData.selectedCustomTimeDateSlotModel!.date!.date} at ${selectedTimeSlot}".toText(fontSize: 12, isBold: true), + ]), + Row(children: [ + "Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + (scheduleData.appointmentType == 2 ? "Home" : "Workshop").toText(fontSize: 12, isBold: true), + ]), + ], + ), + ), + 10.height, + Divider(thickness: 0.7), + Builder(builder: (BuildContext context) { + double totalServicePrice = 0.0; + double totalKms = 15.3; + double rangePricePerKm = 5; + totalServicePrice = rangePricePerKm * totalKms; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Amount".toText(fontSize: 16, isBold: true), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Service Charges".toText(fontSize: 14, color: MyColors.lightTextColor, isBold: true), + "${scheduleData.amountTotal.toString()} SAR".toText(fontSize: 16, isBold: true), + ], + ), + if (scheduleData.appointmentType == 1) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Location Charges ($rangePricePerKm x $totalKms )".toText(fontSize: 14, color: MyColors.lightTextColor, isBold: true), + "${totalServicePrice.toString()} SAR".toText(fontSize: 16, isBold: true), + ], + ), + ], + 10.height, + ], + ); + }), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) => Divider(thickness: 2), + ).paddingOnly(bottom: 10), + ], + ), + ], + ).toWhiteContainer(width: double.infinity, allPading: 12, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 0)); + }, + ); + } + + Widget buildNextButtonFooter({required BuildContext context}) { + AppointmentsVM appointmentsVM = context.read(); + return Container( + color: MyColors.white, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Divider(thickness: 0.7, height: 3), + 8.height, + if (appointmentsVM.amountToPayForAppointment > 0) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Payable now".toText(fontSize: 14, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + appointmentsVM.amountToPayForAppointment.toString().toText(fontSize: 16, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 12, isBold: true).paddingOnly(bottom: 2), + ], + ) + ], + ).paddingOnly(left: 21, right: 21), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Remaining Amount".toText(fontSize: 14, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + (appointmentsVM.totalAmount - appointmentsVM.amountToPayForAppointment).toString().toText(fontSize: 16, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 12, isBold: true).paddingOnly(bottom: 2), + ], + ) + ], + ).paddingOnly(left: 21, right: 21), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Total Amount ".toText(fontSize: 18, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + appointmentsVM.totalAmount.toString().toText(fontSize: 29, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), + ], + ) + ], + ).paddingOnly(left: 21, right: 21), + 5.height, + SizedBox( + width: double.infinity, + child: ShowFillButton( + maxHeight: 55, + backgroundColor: MyColors.darkPrimaryColor, + title: "Book Appointment", + onPressed: () { + appointmentsVM.onBookAppointmentPressed(context); + }, + ).paddingOnly(bottom: 12, left: 21, right: 21), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: "Review Appointment", + isRemoveBackButton: false, + isDrawerEnabled: false, + onBackButtonTapped: () => Navigator.pop(context), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView( + children: [ + buildBranchInfoCard(context: context), + buildServicesInfoCard(context: context), + ], + ).expand(), + buildNextButtonFooter(context: context), + ], + ), + ); + } +} diff --git a/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart b/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart new file mode 100644 index 0000000..979df51 --- /dev/null +++ b/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:mc_common_app/config/routes.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/widgets_models.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:mc_common_app/widgets/txt_field.dart'; +import 'package:provider/provider.dart'; + +class AppointmentServicePickBottomSheet extends StatelessWidget { + const AppointmentServicePickBottomSheet({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + AppointmentsVM appointmentsVM = context.watch(); + return SizedBox( + height: MediaQuery.of(context).size.height * 0.85, + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(8), + height: 8, + width: 60, + decoration: const BoxDecoration(color: MyColors.lightTextColor, borderRadius: BorderRadius.all(Radius.circular(20))), + ), + 12.height, + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "Select Category".toText(fontSize: 24, isBold: true), + ], + ), + 30.height, + Expanded( + child: ListView( + children: [ + Builder( + builder: (context) { + return DropdownField( + (DropValue value) => appointmentsVM.updateProviderCategoryId(SelectionModel(selectedId: value.id, selectedOption: value.value, itemPrice: value.subValue)), + list: appointmentsVM.branchCategories, + hint: "Select Category", + dropdownValue: appointmentsVM.branchSelectedCategoryId.selectedId != -1 + ? DropValue(appointmentsVM.branchSelectedCategoryId.selectedId, appointmentsVM.branchSelectedCategoryId.selectedOption, "") + : null, + ); + }, + ), + if (appointmentsVM.isFetchingServices) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [const CircularProgressIndicator().paddingAll(10)], + ), + ] else if (appointmentsVM.branchServices.isNotEmpty) ...[ + 8.height, + Builder( + builder: (context) { + List serviceCategories = []; + for (var element in appointmentsVM.branchServices) { + if (element.categoryId == appointmentsVM.branchSelectedCategoryId.selectedId) { + serviceCategories.add(DropValue( + element.serviceProviderServiceId ?? 0, + element.serviceDescription!, + "", + )); + } + } + + return DropdownField( + (DropValue value) => appointmentsVM.updateBranchServiceId(SelectionModel(selectedId: value.id, selectedOption: value.value, itemPrice: value.subValue)), + list: serviceCategories, + hint: "Select Service", + dropdownValue: appointmentsVM.branchSelectedServiceId.selectedId != -1 + ? DropValue(appointmentsVM.branchSelectedServiceId.selectedId, appointmentsVM.branchSelectedServiceId.selectedOption, "") + : null, + ); + }, + ), + ], + if (appointmentsVM.branchSelectedServiceId.selectedId != -1 && !appointmentsVM.isFetchingServices) ...[ + 16.height, + Row( + children: [ + "Select Service Location".toText( + fontSize: 16, + isBold: true, + color: MyColors.black, + ), + ], + ), + 8.height, + Row( + children: [ + Expanded( + child: ShowFillButton( + isFilled: appointmentsVM.isHomeTapped, + maxHeight: 48, + title: "Home", + txtColor: appointmentsVM.isHomeTapped ? MyColors.white : MyColors.darkTextColor, + onPressed: () => appointmentsVM.updateIsHomeTapped(true), + ), + ), + 12.width, + Expanded( + child: ShowFillButton( + isFilled: !appointmentsVM.isHomeTapped, + txtColor: !appointmentsVM.isHomeTapped ? MyColors.white : MyColors.darkTextColor, + maxHeight: 48, + title: "Workshop", + onPressed: () => appointmentsVM.updateIsHomeTapped(false), + ), + ), + ], + ), + if (appointmentsVM.isHomeTapped) ...[ + 8.height, + TxtField( + hint: 'Pick Home Location', + errorValue: appointmentsVM.pickHomeLocationError, + value: appointmentsVM.pickedHomeLocation, + isNeedClickAll: true, + postfixData: Icons.location_on, + postFixDataColor: MyColors.darkTextColor, + onTap: () { + //TODO: open the place picked to pick the location and save it in provider. + appointmentsVM.updatePickedHomeLocation("PM58+F97, Al Olaya, Riyadh 12333"); + }, + ), + 14.height, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Icon( + Icons.warning, + color: MyColors.adPendingStatusColor, + size: 19, + ).paddingOnly(bottom: 2), + 3.width, + "Some services are not available on home location.".toText( + color: MyColors.adPendingStatusColor, + fontSize: 12, + isItalic: true, + ), + ], + ), + ] + ], + ], + ), + ), + SizedBox( + width: double.infinity, + child: Column( + children: [ + if (appointmentsVM.isHomeTapped && !appointmentsVM.isFetchingServices) ...[ + const Divider(thickness: 1, height: 1), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // TODO: This Price will be decided according to the service selected + 150.toString().toText(fontSize: 30, isBold: true), + "SAR".toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor).paddingOnly(bottom: 5), + ], + ), + "These charges are additional to the actual service charges. For heavy items the charges may vary.".toText(fontSize: 12, color: MyColors.lightTextColor), + 22.height, + ], + SizedBox( + width: double.infinity, + child: ShowFillButton( + maxHeight: 55, + backgroundColor: !appointmentsVM.isServiceSelectionValidated() ? MyColors.lightTextColor.withOpacity(0.6) : MyColors.darkPrimaryColor, + title: "Next", + onPressed: () { + bool isValidated = appointmentsVM.isServiceSelectionValidated(); + if (isValidated) { + appointmentsVM.getServiceItems(appointmentsVM.branchSelectedServiceId.selectedId); + Navigator.pop(context); + navigateWithName(context, AppRoutes.bookAppointmentsItemView); + } + }, + ), + ), + ], + ), + ).paddingOnly(bottom: 20) + ], + )).horPaddingMain(); + } +} diff --git a/lib/views/appointments/widgets/custom_calender_widget.dart b/lib/views/appointments/widgets/custom_calender_widget.dart new file mode 100644 index 0000000..4be5587 --- /dev/null +++ b/lib/views/appointments/widgets/custom_calender_widget.dart @@ -0,0 +1,238 @@ + +import 'package:flutter/material.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/service_schedule_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; +import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; +import 'package:table_calendar/table_calendar.dart'; + +class CustomCalenderWidget extends StatefulWidget { + final List customTimeDateSlotList; + final int scheduleIndex; + + const CustomCalenderWidget({super.key, required this.customTimeDateSlotList, required this.scheduleIndex}); + + @override + State createState() => _CustomCalenderWidgetState(); +} + +class _CustomCalenderWidgetState extends State { + List allDates = []; + List allMonths = []; + List datesInSelectedMonth = []; + final CalendarFormat _calendarFormat = CalendarFormat.month; + + late int selectedMonth; + late int selectedYear; + + DateTime? _selectedDay; + + DateTime _focusedDay = DateTime.now(); + + @override + void initState() { + super.initState(); + populateDateList(); + } + + populateDateList() { + for (var value in widget.customTimeDateSlotList) { + DateTime dt = DateFormat('dd MMMM, yyyy').parse(value.date!.date); + DropValue dv = DropValue(dt.month, "${dt.month.getMonthNameByNumber()}, ${dt.year}", "${dt.year}"); + allDates.add(dt); + + if (!ifMonthAlreadyThere(dv)) { + allMonths.add(dv); + } + } + selectedMonth = allDates.first.month; + selectedYear = allDates.first.year; + final appointmentsVM = context.read(); + if (appointmentsVM.serviceAppointmentScheduleList[widget.scheduleIndex].selectedCustomTimeDateSlotModel != null) { + DateTime alreadySelectedDate = DateFormat('dd MMMM, yyyy').parse(appointmentsVM.serviceAppointmentScheduleList[widget.scheduleIndex].selectedCustomTimeDateSlotModel!.date!.date); + _selectedDay = alreadySelectedDate; + _focusedDay = alreadySelectedDate; + selectedMonth = alreadySelectedDate.month; + selectedYear = alreadySelectedDate.year; + } + datesInSelectedMonth = allDates.where((element) => element.month == selectedMonth).toList(); + + if (appointmentsVM.serviceAppointmentScheduleList[widget.scheduleIndex].selectedCustomTimeDateSlotModel == null) { + _focusedDay = datesInSelectedMonth.first; + } + } + + bool ifMonthAlreadyThere(DropValue monthDate) { + int index = allMonths.indexWhere((element) => element.id == monthDate.id && element.subValue == monthDate.subValue); + if (index == -1) { + return false; + } + return true; + } + + bool ifDateAlreadyThere(DateTime dt) { + int index = allDates.indexWhere((element) => dt.month == element.month && dt.year == element.year); + if (index == -1) { + return false; + } + return true; + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Builder(builder: (context) { + return DropdownField( + (DropValue value) { + setState(() { + selectedYear = int.parse(value.value.split(',')[1]); + selectedMonth = value.value.split(',')[0].getMonthNumberByName(); + datesInSelectedMonth = allDates.where((element) => element.month == selectedMonth).toList(); + _focusedDay = datesInSelectedMonth.first; + }); + }, + list: allMonths, + dropdownValue: DropValue(selectedMonth, "${selectedMonth.getMonthNameByNumber()}, ${selectedYear}", "${selectedYear}"), + hint: "${selectedMonth.getMonthNameByNumber()}, $selectedYear", + errorValue: "", + showAppointmentPickerVariant: true, + ); + }), + ), + Spacer(), + Icon( + Icons.calendar_today, + color: Colors.black, + size: 18, + ).paddingOnly(right: 10) + ], + ).paddingOnly(left: 6, right: 0), + TableCalendar( + headerVisible: false, + firstDay: datesInSelectedMonth.first, + lastDay: datesInSelectedMonth.last, + focusedDay: _focusedDay, + calendarFormat: _calendarFormat, + weekendDays: [DateTime.friday, DateTime.saturday], + daysOfWeekHeight: 30, + availableGestures: AvailableGestures.none, + daysOfWeekStyle: DaysOfWeekStyle( + weekdayStyle: TextStyle(fontSize: 14, color: MyColors.black), + weekendStyle: TextStyle(fontSize: 14, color: MyColors.black.withOpacity(0.5)), + ), + calendarBuilders: CalendarBuilders( + selectedBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MyColors.darkIconColor, + ), + alignment: Alignment.center, + child: Text( + dateTime2.day.toString(), + style: TextStyle(color: MyColors.white), + ), + ); + }, + defaultBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { + int index = datesInSelectedMonth.indexWhere((element) => isSameDay(dateTime1, element)); + if (index == -1) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + dateTime1.day.toString(), + ), + ); + } + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all(color: Colors.orange, width: 2), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + dateTime1.day.toString(), + ), + ); + }, + disabledBuilder: (BuildContext context, DateTime dateTime1, DateTime selectedDt) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + dateTime1.day.toString(), + ), + ); + }, + ), + headerStyle: HeaderStyle(formatButtonVisible: false), + selectedDayPredicate: (day) { + // Use `selectedDayPredicate` to determine which day is currently selected. + // If this returns true, then `day` will be marked as selected. + + // Using `isSameDay` is recommended to disregard + // the time-part of compared DateTime objects. + return isSameDay(_selectedDay, day); + }, + onDaySelected: (selectedDay, focusedDay) { + int index = datesInSelectedMonth.indexWhere((element) => isSameDay(selectedDay, element)); + + if (index == -1) { + return; + } + if (!isSameDay(_selectedDay, selectedDay)) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + + int dateIndex = widget.customTimeDateSlotList.indexWhere((element) { + DateTime dateFromList = DateFormat('dd MMMM, yyyy').parse(element.date!.date); + if (isSameDay(dateFromList, selectedDay)) { + return true; + } + return false; + }); + + if (dateIndex != -1) { + appointmentsVM.updateSelectedAppointmentDate(scheduleIndex: widget.scheduleIndex, dateIndex: dateIndex); + } + } + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/views/appointments/widgets/customer_appointment_slider_widget.dart b/lib/views/appointments/widgets/customer_appointment_slider_widget.dart new file mode 100644 index 0000000..d795747 --- /dev/null +++ b/lib/views/appointments/widgets/customer_appointment_slider_widget.dart @@ -0,0 +1,209 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/config/routes.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/view_models/dashboard_view_model_customer.dart'; +import 'package:mc_common_app/views/advertisement/custom_add_button.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; + +class CustomerAppointmentSliderWidget extends StatelessWidget { + final List myUpComingAppointments; + + const CustomerAppointmentSliderWidget({Key? key, required this.myUpComingAppointments}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (myUpComingAppointments.isEmpty) { + return CustomAddButton( + needsBorder: true, + bgColor: MyColors.white, + onTap: () => context.read().onNavbarTapped(1), + text: "Add New Appointment", + icon: Container( + height: 24, + width: 24, + decoration: const BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), + child: const Icon(Icons.add, color: MyColors.white), + ), + ).padding(EdgeInsets.symmetric(vertical: 10, horizontal: 21)); + return Container( + height: 86, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 24, + width: 24, + decoration: BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), + child: Icon( + Icons.add, + color: MyColors.white, + ), + ), + SizedBox(width: 10), + "Add New Appointment".toText( + fontSize: 15, + isBold: true, + color: MyColors.lightTextColor, + ), + ], + ), + ).onPress(() {}).toWhiteContainer(width: double.infinity, margin: EdgeInsets.symmetric(horizontal: 21, vertical: 10)); + } + return CarouselSlider.builder( + options: CarouselOptions( + height: 140, + viewportFraction: 1.0, + enlargeCenterPage: false, + enableInfiniteScroll: false, + ), + itemCount: myUpComingAppointments.length, + itemBuilder: (BuildContext context, int itemIndex, int pageViewIndex) => BuildAppointmentContainerForCustomer( + isForHome: true, + appointmentListModel: myUpComingAppointments[itemIndex], + onTapped: () => navigateWithName(context, AppRoutes.appointmentDetailView, arguments: myUpComingAppointments[itemIndex]), + ), + ); + } +} + +class BuildAppointmentContainerForCustomer extends StatelessWidget { + final bool? isForHome; + final AppointmentListModel? appointmentListModel; + final Function() onTapped; + + const BuildAppointmentContainerForCustomer({Key? key, this.isForHome = false, required this.onTapped, required this.appointmentListModel}) : super(key: key); + + Widget showServices(String title, String icon, {bool isMoreText = false}) { + return Row( + children: [ + if (icon != "") ...[ + SvgPicture.asset(icon), + 8.width, + ], + Flexible( + child: title.toText( + fontSize: 12, + isBold: true, + color: isMoreText ? MyColors.primaryColor : MyColors.black, + ), + ), + ], + ); + } + + List buildServicesFromAppointment({required AppointmentListModel appointmentListModel}) { + if (appointmentListModel.appointmentServicesList == null || appointmentListModel.appointmentServicesList!.isEmpty) { + return [SizedBox()]; + } + + if (appointmentListModel.appointmentServicesList!.length == 1) { + return [ + showServices( + appointmentListModel.appointmentServicesList![0].providerServiceDescription, + MyAssets.modificationsIcon, + ) + ]; + } + + List servicesList = List.generate( + 2, + (index) => showServices(appointmentListModel.appointmentServicesList![index].providerServiceDescription, MyAssets.modificationsIcon), + ); + + if (appointmentListModel.appointmentServicesList!.length > 1) { + servicesList.add( + showServices( + "+ ${appointmentListModel.appointmentServicesList!.length - 1} More", + "", + isMoreText: true, + ), + ); + } + return servicesList; + } + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 10, left: 21, right: 21), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + isForHome != null && isForHome! + ? Image.asset( + MyAssets.bnCar, + width: 56, + height: 56, + fit: BoxFit.fill, + ).toCircle(borderRadius: 100) + : Image.asset( + MyAssets.bnCar, + width: 80, + height: 85, + fit: BoxFit.cover, + ), + 8.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + (appointmentListModel!.providerName ?? "").toText(color: MyColors.black, isBold: true, fontSize: 16), + Row( + children: [ + MyAssets.miniClock.buildSvg(height: 12), + 2.width, + "${appointmentListModel!.duration ?? ""} ${appointmentListModel!.appointmentDate!.toFormattedDateWithoutTime()}".toText( + color: MyColors.lightTextColor, + fontSize: 12, + ), + ], + ), + 9.height, + isForHome != null && isForHome! + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + "Appointment Details".toText( + color: MyColors.primaryColor, + isUnderLine: true, + isBold: true, + fontSize: 14, + ), + const Icon(Icons.arrow_forward), + ], + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + children: buildServicesFromAppointment(appointmentListModel: appointmentListModel!), + ), + ), + const Icon( + Icons.arrow_forward, + ), + ], + ), + ], + ), + ), + ], + ), + ], + ).onPress(onTapped).toWhiteContainer(width: double.infinity, allPading: 12), + ); + } +} diff --git a/lib/views/appointments/widgets/service_item_with_price_checkbox.dart b/lib/views/appointments/widgets/service_item_with_price_checkbox.dart new file mode 100644 index 0000000..f8269c6 --- /dev/null +++ b/lib/views/appointments/widgets/service_item_with_price_checkbox.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; + +class ServiceItemWithPriceCheckBox extends StatelessWidget { + final bool isSelected; + final String title, description; + final Widget priceWidget; + + final Function(bool) onSelection; + + const ServiceItemWithPriceCheckBox({ + required this.isSelected, + required this.title, + required this.description, + this.priceWidget = const SizedBox(), + required this.onSelection, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Checkbox( + value: isSelected, + onChanged: (bool? v) { + onSelection(v ?? false); + }, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + title.toText(fontSize: 16, isBold: true).paddingOnly(top: 10), + description.toText(fontSize: 12, color: MyColors.lightTextColor), + priceWidget, + ], + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 18dfded..efad35d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -861,6 +861,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + sha256: "86d08f85f1f58583b7b4b941d989f48ea6ce08c1724a1d10954a277c2ec36592" + url: "https://pub.dev" + source: hosted + version: "0.2.0" sizer: dependency: "direct main" description: @@ -946,6 +954,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + table_calendar: + dependency: "direct main" + description: + name: table_calendar + sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478" + url: "https://pub.dev" + source: hosted + version: "3.0.9" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1cd95e8..e6788f3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: dropdown_button2: ^2.0.0 flutter_inappwebview: ^5.7.2+3 country_code_picker: ^3.0.0 + table_calendar: ^3.0.9 + # google google_maps_flutter: ^2.1.1